freesolo-flash-dev 1.0.1__tar.gz → 1.0.2__tar.gz

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.
Files changed (341) hide show
  1. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/PKG-INFO +1 -1
  2. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/training_doc.py +42 -0
  3. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/runner/verified_revisions.py +7 -1
  4. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/serve/export.py +60 -17
  5. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/_deps.py +35 -4
  6. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/routes/runs.py +22 -6
  7. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/pyproject.toml +2 -2
  8. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_export.py +78 -6
  9. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_api.py +109 -0
  10. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_verified_revisions.py +29 -0
  11. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/uv.lock +1 -1
  12. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.claude/skills/verify/SKILL.md +0 -0
  13. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.dockerignore +0 -0
  14. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.env.example +0 -0
  15. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/auto-rebake.yml +0 -0
  16. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/bake-kernel-cache.yml +0 -0
  17. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/ci.yml +0 -0
  18. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/main-source-guard.yml +0 -0
  19. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/notify-tests-repo.yml +0 -0
  20. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/publish-dev.yml +0 -0
  21. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/publish-image.yml +0 -0
  22. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/publish.yml +0 -0
  23. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/version-parity.yml +0 -0
  24. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.github/workflows/worker-image.yml +0 -0
  25. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/.gitignore +0 -0
  26. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/Dockerfile +0 -0
  27. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/Dockerfile.worker +0 -0
  28. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/LICENSE +0 -0
  29. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/README.md +0 -0
  30. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/build/kernel_cache/.gitignore +0 -0
  31. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/build/kernel_cache/.keep +0 -0
  32. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/docker/Dockerfile.kernelcache +0 -0
  33. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/docker/Dockerfile.kernelcache.relayer +0 -0
  34. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/docker/bake_kernel_cache.py +0 -0
  35. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/docker/bake_pod_entry.py +0 -0
  36. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/docker/kernel_fingerprint.py +0 -0
  37. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/docker/make_rp_handler.py +0 -0
  38. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/__init__.py +0 -0
  39. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/_channel.py +0 -0
  40. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/_fileio.py +0 -0
  41. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/_logging.py +0 -0
  42. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/_update_check.py +0 -0
  43. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/adapter_artifacts.py +0 -0
  44. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/catalog.py +0 -0
  45. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/__init__.py +0 -0
  46. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/__main__.py +0 -0
  47. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/_tty.py +0 -0
  48. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/commands.py +0 -0
  49. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/env_setup.py +0 -0
  50. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/envpush.py +0 -0
  51. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cli/render.py +0 -0
  52. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/client/__init__.py +0 -0
  53. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/client/config.py +0 -0
  54. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/client/http.py +0 -0
  55. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/client/runtime_secrets.py +0 -0
  56. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/client/specs.py +0 -0
  57. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cost/__init__.py +0 -0
  58. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cost/analytical.py +0 -0
  59. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cost/facts.py +0 -0
  60. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cost/spec.py +0 -0
  61. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/cost/types.py +0 -0
  62. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/diagnostics.py +0 -0
  63. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/__init__.py +0 -0
  64. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/accounting.py +0 -0
  65. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/chalk_kernels.py +0 -0
  66. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/multiturn_rollout.py +0 -0
  67. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/recipe.py +0 -0
  68. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/steps.py +0 -0
  69. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/structured_outputs.py +0 -0
  70. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/vram.py +0 -0
  71. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/__init__.py +0 -0
  72. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/__main__.py +0 -0
  73. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/_pkg.py +0 -0
  74. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/adapter.py +0 -0
  75. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/decoding.py +0 -0
  76. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/finalize.py +0 -0
  77. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/gpu_setup.py +0 -0
  78. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/grpo.py +0 -0
  79. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/heartbeat.py +0 -0
  80. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/hf.py +0 -0
  81. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/kernel_warmup.py +0 -0
  82. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/lora.py +0 -0
  83. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/opd.py +0 -0
  84. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/opd_gkd.py +0 -0
  85. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/opd_vllm.py +0 -0
  86. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/packing.py +0 -0
  87. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/__init__.py +0 -0
  88. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/attn.py +0 -0
  89. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/diagnostics.py +0 -0
  90. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/lifecycle.py +0 -0
  91. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/liger.py +0 -0
  92. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/loraplus.py +0 -0
  93. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/perf/memory.py +0 -0
  94. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/rl.py +0 -0
  95. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/rng.py +0 -0
  96. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/sft.py +0 -0
  97. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/teacher.py +0 -0
  98. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/tokenizer_align.py +0 -0
  99. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker/wandb_log.py +0 -0
  100. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/engine/worker_entrypoint.py +0 -0
  101. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/__init__.py +0 -0
  102. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/adapter.py +0 -0
  103. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/archive.py +0 -0
  104. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/archive_policy.py +0 -0
  105. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/base.py +0 -0
  106. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/loader.py +0 -0
  107. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/pull.py +0 -0
  108. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/envs/registry.py +0 -0
  109. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/lora_rank.py +0 -0
  110. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/opd_retry_contract.py +0 -0
  111. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/__init__.py +0 -0
  112. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_auth.py +0 -0
  113. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_deadline.py +0 -0
  114. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_hf_artifacts.py +0 -0
  115. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_hf_retry.py +0 -0
  116. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_http.py +0 -0
  117. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_instance.py +0 -0
  118. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_instance_bootstrap.py +0 -0
  119. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_instance_poll.py +0 -0
  120. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_instance_provider.py +0 -0
  121. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_poll.py +0 -0
  122. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/_worker.py +0 -0
  123. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/allocator.py +0 -0
  124. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/base.py +0 -0
  125. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/__init__.py +0 -0
  126. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/api.py +0 -0
  127. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/auth.py +0 -0
  128. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/gpus.py +0 -0
  129. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/jobs/__init__.py +0 -0
  130. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/jobs/builders.py +0 -0
  131. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/preflight.py +0 -0
  132. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/lambdalabs/pricing.py +0 -0
  133. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/preflight.py +0 -0
  134. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/realized.py +0 -0
  135. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/__init__.py +0 -0
  136. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/api.py +0 -0
  137. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/auth.py +0 -0
  138. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/cost.py +0 -0
  139. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/gpus.py +0 -0
  140. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/jobs.py +0 -0
  141. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/keys.py +0 -0
  142. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/preflight.py +0 -0
  143. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/preload.py +0 -0
  144. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/pricing.py +0 -0
  145. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/slots.py +0 -0
  146. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/train/__init__.py +0 -0
  147. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/runpod/train/endpoints.py +0 -0
  148. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/__init__.py +0 -0
  149. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/api.py +0 -0
  150. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/auth.py +0 -0
  151. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/jobs/__init__.py +0 -0
  152. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/jobs/builders.py +0 -0
  153. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/preflight.py +0 -0
  154. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/providers/vast/pricing.py +0 -0
  155. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/py.typed +0 -0
  156. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/runner/__init__.py +0 -0
  157. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/runner/checkpoints.py +0 -0
  158. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/runner/deploy.py +0 -0
  159. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/runner/lifecycle.py +0 -0
  160. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/schema/__init__.py +0 -0
  161. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/schema/fields.py +0 -0
  162. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/serve/__init__.py +0 -0
  163. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/serve/deploy.py +0 -0
  164. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/serve/pricing.py +0 -0
  165. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/serve/urls.py +0 -0
  166. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/__init__.py +0 -0
  167. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/__main__.py +0 -0
  168. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/_internal_client.py +0 -0
  169. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/_locks.py +0 -0
  170. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/_runtime.py +0 -0
  171. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/app.py +0 -0
  172. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/auth.py +0 -0
  173. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/billing.py +0 -0
  174. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/billing_retry.py +0 -0
  175. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/checkpoints.py +0 -0
  176. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/db.py +0 -0
  177. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/environment_registry.py +0 -0
  178. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/envs.py +0 -0
  179. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/reconcile.py +0 -0
  180. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/repo_cleanup.py +0 -0
  181. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/routes/__init__.py +0 -0
  182. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/routes/envs.py +0 -0
  183. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/routes/meta.py +0 -0
  184. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/routes/serving.py +0 -0
  185. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/server/run_registry.py +0 -0
  186. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/flash/spec.py +0 -0
  187. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/infisical-entrypoint.sh +0 -0
  188. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/scripts/build_dev_dist.py +0 -0
  189. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/__init__.py +0 -0
  190. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/_helpers/__init__.py +0 -0
  191. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/_helpers/runner.py +0 -0
  192. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/_helpers/specs.py +0 -0
  193. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/_helpers/vast.py +0 -0
  194. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/conftest.py +0 -0
  195. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/fixtures/math_eval.jsonl +0 -0
  196. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/fixtures/math_train.jsonl +0 -0
  197. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/live/__init__.py +0 -0
  198. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/live/conftest.py +0 -0
  199. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/live/test_lambda_live.py +0 -0
  200. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/live/test_runpod_live.py +0 -0
  201. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/live/test_vast_live.py +0 -0
  202. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_accounting_coverage.py +0 -0
  203. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_agent_flash_cli_contract.py +0 -0
  204. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_algorithms.py +0 -0
  205. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_allocator.py +0 -0
  206. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_b200_rollout_opt.py +0 -0
  207. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_backend_jobspec_contract.py +0 -0
  208. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_base_model_provenance.py +0 -0
  209. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_billing_retry.py +0 -0
  210. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cancel_remote.py +0 -0
  211. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_catalog_consistency.py +0 -0
  212. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_chalk_kernels.py +0 -0
  213. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_charge_pricing.py +0 -0
  214. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_checkpoints.py +0 -0
  215. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_commands.py +0 -0
  216. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_commands_coverage.py +0 -0
  217. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_errors.py +0 -0
  218. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_estimate.py +0 -0
  219. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_help.py +0 -0
  220. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_main_coverage.py +0 -0
  221. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_managed.py +0 -0
  222. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_render_coverage.py +0 -0
  223. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cli_render_theme.py +0 -0
  224. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_client.py +0 -0
  225. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_client_server_integration.py +0 -0
  226. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_config_overrides.py +0 -0
  227. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_controlled_experiment_repairs.py +0 -0
  228. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cost_analytical.py +0 -0
  229. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cost_equation.py +0 -0
  230. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cost_estimate.py +0 -0
  231. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cost_hardware.py +0 -0
  232. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cost_models.py +0 -0
  233. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_cost_rewards.py +0 -0
  234. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_dev_channel.py +0 -0
  235. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_disk_gb.py +0 -0
  236. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_endpoint_name.py +0 -0
  237. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_cache_evict.py +0 -0
  238. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_delete.py +0 -0
  239. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_download.py +0 -0
  240. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_download_api.py +0 -0
  241. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_no_install.py +0 -0
  242. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_publish.py +0 -0
  243. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_pull.py +0 -0
  244. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_pull_coverage.py +0 -0
  245. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_pull_loader_coverage.py +0 -0
  246. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_pull_managed_control_plane.py +0 -0
  247. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_push.py +0 -0
  248. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_env_rate_limit_resolve.py +0 -0
  249. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_envs_coverage.py +0 -0
  250. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_flash_mvp.py +0 -0
  251. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_flash_worker.py +0 -0
  252. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_flashinfer_cache_dirs.py +0 -0
  253. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_github_urlopen_retry.py +0 -0
  254. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_gpus.py +0 -0
  255. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_grpo_mask_aware.py +0 -0
  256. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_grpo_params.py +0 -0
  257. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_grpo_sleep_gate.py +0 -0
  258. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_health_liveness.py +0 -0
  259. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_hf_retry.py +0 -0
  260. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_idle_endpoint_reaper.py +0 -0
  261. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_instance_bootstrap_coverage.py +0 -0
  262. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_internal_client.py +0 -0
  263. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_jobs.py +0 -0
  264. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_kernel_cache.py +0 -0
  265. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_kernel_fingerprint.py +0 -0
  266. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_kv_util.py +0 -0
  267. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_lambda_api_coverage.py +0 -0
  268. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_lambda_runner.py +0 -0
  269. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_logging.py +0 -0
  270. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_login_perms.py +0 -0
  271. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_lora_rank_coverage.py +0 -0
  272. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_lora_rank_preflight.py +0 -0
  273. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_managed_hf_repo.py +0 -0
  274. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_metrics_schema_agent_contract.py +0 -0
  275. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_mig_guard.py +0 -0
  276. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_multiturn_rollout.py +0 -0
  277. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_multiturn_rollout_coverage.py +0 -0
  278. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_multiturn_rollout_records.py +0 -0
  279. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_multiturn_rollout_request_policy.py +0 -0
  280. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_oom_escalate_gpu.py +0 -0
  281. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_opd.py +0 -0
  282. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_opd_full_state_resume.py +0 -0
  283. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_opd_resume_safety.py +0 -0
  284. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_opd_vllm.py +0 -0
  285. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_open_model_policy.py +0 -0
  286. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_orchestrator_flash.py +0 -0
  287. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_packing.py +0 -0
  288. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_packing_coverage.py +0 -0
  289. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_poll_helpers.py +0 -0
  290. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_preflight.py +0 -0
  291. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_provider_preflight_coverage.py +0 -0
  292. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_provider_routing.py +0 -0
  293. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_provider_teardown_robustness.py +0 -0
  294. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_providers_symmetry.py +0 -0
  295. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_realized_cost.py +0 -0
  296. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_repo_cleanup.py +0 -0
  297. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_resolve_params_b.py +0 -0
  298. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_resume_on_retry.py +0 -0
  299. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_runmgmt.py +0 -0
  300. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_runpod_api_delete.py +0 -0
  301. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_runpod_key_fingerprint.py +0 -0
  302. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_runpod_key_waterfall.py +0 -0
  303. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_runpod_slots.py +0 -0
  304. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_serve.py +0 -0
  305. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_app_coverage.py +0 -0
  306. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_billing.py +0 -0
  307. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_checkpoints_coverage.py +0 -0
  308. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_db.py +0 -0
  309. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_envs_coverage.py +0 -0
  310. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_server_locks_coverage.py +0 -0
  311. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_serving_context_preflight.py +0 -0
  312. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_serving_contract.py +0 -0
  313. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_serving_pricing_coverage.py +0 -0
  314. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_serving_schema_coverage.py +0 -0
  315. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_sft_example_selection.py +0 -0
  316. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_sft_gc_off.py +0 -0
  317. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_sft_max_context.py +0 -0
  318. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_spec_and_validation.py +0 -0
  319. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_structured_outputs.py +0 -0
  320. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_thinking_config.py +0 -0
  321. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_train_context_preflight.py +0 -0
  322. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_training_controls.py +0 -0
  323. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_update_check.py +0 -0
  324. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_vast_api.py +0 -0
  325. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_vast_api_coverage.py +0 -0
  326. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_vast_offers.py +0 -0
  327. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_vast_provider_coverage.py +0 -0
  328. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_vast_runner.py +0 -0
  329. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_verifiers.py +0 -0
  330. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_version.py +0 -0
  331. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_vl_warmstart_adapter_keys.py +0 -0
  332. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_wandb_log_coverage.py +0 -0
  333. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_wandb_naming.py +0 -0
  334. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_warmstart_cross_repo.py +0 -0
  335. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_weight_cache.py +0 -0
  336. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_worker_dryrun.py +0 -0
  337. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_worker_hardexit.py +0 -0
  338. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_worker_image.py +0 -0
  339. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_worker_init_heartbeat.py +0 -0
  340. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_worker_stack.py +0 -0
  341. {freesolo_flash_dev-1.0.1 → freesolo_flash_dev-1.0.2}/tests/test_worker_thinking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freesolo-flash-dev
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Flash — managed LoRA post-training (SFT/GRPO/OPD) for Freesolo environments, driven by the `flash` CLI
5
5
  Project-URL: Homepage, https://github.com/freesolo-co/flash
6
6
  Project-URL: Repository, https://github.com/freesolo-co/flash
@@ -154,6 +154,47 @@ flash undeploy <run-id> # tear the endpoint down
154
154
  flash export --adapter-id <run-id> --repository <you>/<repo> # copy adapter weights to your HF repo
155
155
  ```
156
156
 
157
+ ### Loading an exported adapter locally (transformers + peft)
158
+
159
+ Flash trains the Qwen3.5/3.6 family against the full multimodal module tree, so adapter
160
+ weights saved by the worker carry a `language_model.` infix in their tensor keys
161
+ (`base_model.model.model.language_model.layers.*`). During `flash export`, adapters that
162
+ can be represented in the text-only namespace are normalized to
163
+ `base_model.model.model.layers.*`. When an export retains non-LM tensors, it keeps the
164
+ multimodal namespace instead. Do not infer the namespace solely from whether `flash export`
165
+ was used: inspect the keys. For normalized keys, load with vanilla peft and
166
+ `AutoModelForCausalLM`:
167
+
168
+ ```python
169
+ from peft import PeftModel
170
+ from transformers import AutoModelForCausalLM
171
+
172
+ base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.5-0.8B") # qwen3.5 causal-lm class
173
+ model = PeftModel.from_pretrained(base, "<you>/<repo>")
174
+ ```
175
+
176
+ **Before you trust any local eval, check the key namespace matches the model class you
177
+ loaded.** peft does NOT error on mismatched keys — it emits a `UserWarning` about missing
178
+ adapter keys and silently applies *nothing*, so you would benchmark the bare base model
179
+ believing it is your adapter. If the inspected keys carry the `language_model.` infix,
180
+ load the base with the multimodal class whose parameters live under that namespace instead:
181
+
182
+ ```python
183
+ # infixed keys (base_model.model.model.language_model.layers.*) need the multimodal class:
184
+ from transformers import AutoModelForImageTextToText
185
+
186
+ base = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen3.5-0.8B")
187
+ # qwen3.5/3.6 dense models resolve to Qwen3_5ForConditionalGeneration
188
+ # qwen3.6-35b-a3b (moe) resolves to Qwen3_5MoeForConditionalGeneration
189
+ model = PeftModel.from_pretrained(base, "<you>/<repo>")
190
+ ```
191
+
192
+ To check which form you have, read the safetensors key names (stdlib, no torch needed):
193
+ the JSON header of `adapter_model.safetensors` lists every tensor key. Rule of thumb:
194
+ `model.layers.*` keys pair with `AutoModelForCausalLM`; `model.language_model.layers.*`
195
+ keys pair with the `*ForConditionalGeneration` class. After loading, confirm the adapter
196
+ actually applied: outputs (or a probe metric) must differ from the bare base model.
197
+
157
198
  The rest of this file is about doing the above *well* — designing a reward that teaches,
158
199
  and deciding honestly whether a run improved.
159
200
 
@@ -221,6 +262,7 @@ spending another GPU run:
221
262
  | Run looks stuck after disconnecting | Terminal stopped streaming but the job may still be alive | Ctrl-C detaches. Use `flash log <run-id> --follow` to reattach, `flash log <run-id>` for the console/error output, or `flash cancel <run-id>` if you intentionally want to stop it. |
222
263
  | Final checkpoint regresses | Last step is worse than an earlier checkpoint | Run `flash checkpoints <run-id>`, deploy a specific step with `flash deploy <run-id>/step-N`, and compare with held-out probes before exporting or relying on the final adapter. |
223
264
  | Export fails before upload | CLI says no HuggingFace token | Pass `flash export --api-key hf_...`, or set `HF_TOKEN` in your shell, `.env`, or `.env.local`. Exports are private unless you pass `--public`. |
265
+ | Exported adapter is a silent no-op locally | peft warns about missing adapter keys and local eval matches the bare base model | The adapter's key namespace does not match the loaded model class. `model.layers.*` keys pair with `AutoModelForCausalLM`; `model.language_model.layers.*` keys pair with `Qwen3_5ForConditionalGeneration` / `Qwen3_5MoeForConditionalGeneration` (via `AutoModelForImageTextToText`). See "Loading an exported adapter locally". |
224
266
  | SFT loss improves but quality does not | Train loss falls while held-out behavior stalls or degrades | Keep a held-out split outside training. Deploy and score that split; if quality drops, reduce epochs or improve data instead of adding more passes. |
225
267
  | Cost surprises | A quick experiment uses more GPU time than intended | Start with `--dry-run` and `--cost`, keep `epochs` and `max_examples` small for smoke tests, and scale only after reward/data wiring is proven. Setup time is reported for observability; customer cost is based on training-loop GPU time. |
226
268
 
@@ -3,13 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import contextlib
6
- import fcntl
7
6
  import json
8
7
  import os
9
8
  import tempfile
10
9
  from collections.abc import Callable, Iterator
11
10
  from contextlib import contextmanager
12
11
 
12
+ try:
13
+ import fcntl
14
+ except ImportError: # pragma: no cover - linux production fails closed below
15
+ fcntl = None
16
+
13
17
  from flash.schema import parse_adapter_revision
14
18
 
15
19
  _LEDGER_SUFFIX = ".verified-revisions"
@@ -29,6 +33,8 @@ def _paths(run_id: str) -> tuple[str, str, str]:
29
33
 
30
34
  @contextmanager
31
35
  def _locked_ledger(run_id: str, *, exclusive: bool) -> Iterator[tuple[str, str]]:
36
+ if fcntl is None:
37
+ raise RuntimeError("interprocess verified-revision locking is unavailable")
32
38
  runs_dir, ledger_path, lock_path = _paths(run_id)
33
39
  fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
34
40
  with os.fdopen(fd, "a+") as lock_file:
@@ -14,6 +14,7 @@ import shutil
14
14
  import struct
15
15
  import tempfile
16
16
  from pathlib import Path
17
+ from typing import BinaryIO
17
18
 
18
19
  from flash._logging import get_logger
19
20
  from flash.serve.deploy import ServingError
@@ -29,11 +30,11 @@ _TEMP_MERGED_BASE_MODEL_RE = re.compile(
29
30
  _MAX_SAFETENSORS_HEADER_BYTES = 100 * 1024 * 1024
30
31
 
31
32
  # module-path segments outside the language model; mirrors _VL_EXCLUDE_SEGMENTS in
32
- # flash/engine/worker/lora.py. today's managed adapters never touch these (the worker
33
- # excludes them from lora), so their presence means a genuinely multimodal adapter whose
34
- # vision keys the text-only strip cannot represent. normalizing only the lm keys of such
35
- # an adapter would produce a mixed namespace that matches no transformers class, so the
36
- # normalization must skip the file entirely and export it as trained.
33
+ # flash/engine/worker/lora.py. managed adapters can contain inert non-lm lora entries when
34
+ # broad target-module names select vision or mtp modules, so key presence alone is not a
35
+ # reason to preserve the mixed namespace. only a nonzero lora_b contribution, or an
36
+ # unrecognized non-lm tensor, proves the adapter genuinely trains those modules and must
37
+ # keep the file unchanged.
37
38
  _NON_LM_KEY_SEGMENTS = (".visual.", ".vision_tower.", ".multi_modal_projector.", ".mtp.")
38
39
 
39
40
 
@@ -41,6 +42,42 @@ def _references_non_lm_modules(key: str) -> bool:
41
42
  return any(segment in key for segment in _NON_LM_KEY_SEGMENTS)
42
43
 
43
44
 
45
+ def _non_lm_tensor_is_live(
46
+ source: BinaryIO,
47
+ key: str,
48
+ descriptor: object,
49
+ *,
50
+ data_start: int,
51
+ file_size: int,
52
+ ) -> bool:
53
+ is_lora_a = ".lora_A." in key
54
+ is_lora_b = ".lora_B." in key
55
+ if is_lora_a and not is_lora_b:
56
+ return False
57
+ if not is_lora_b or is_lora_a:
58
+ return True
59
+ if not isinstance(descriptor, dict):
60
+ raise ValueError(f"safetensors descriptor for {key!r} is not an object")
61
+ offsets = descriptor.get("data_offsets")
62
+ if (
63
+ not isinstance(offsets, list)
64
+ or len(offsets) != 2
65
+ or any(not isinstance(offset, int) or isinstance(offset, bool) for offset in offsets)
66
+ ):
67
+ raise ValueError(f"safetensors data_offsets for {key!r} are invalid")
68
+ lo, hi = offsets
69
+ data_size = file_size - data_start
70
+ if lo < 0 or hi < lo or hi > data_size:
71
+ raise ValueError(
72
+ f"safetensors data_offsets for {key!r} are outside the {data_size}-byte data section"
73
+ )
74
+ source.seek(data_start + lo)
75
+ tensor_bytes = source.read(hi - lo)
76
+ if len(tensor_bytes) != hi - lo:
77
+ raise ValueError(f"safetensors tensor data for {key!r} is truncated")
78
+ return any(tensor_bytes)
79
+
80
+
44
81
  def _strip_language_model_infix(key: str) -> str:
45
82
  # mirrors _LANGUAGE_MODEL_INFIX namespace semantics in flash/engine/worker/lora.py
46
83
  infix = ".language_model."
@@ -90,18 +127,23 @@ def _normalize_export_adapter_keys(adapter_dir: Path) -> bool:
90
127
  if not isinstance(header, dict):
91
128
  raise ValueError("safetensors header is not a JSON object")
92
129
 
93
- non_lm = [
94
- key
95
- for key in header
96
- if key != "__metadata__" and _references_non_lm_modules(key)
97
- ]
98
- if non_lm:
99
- logger.info(
100
- "exported adapter touches non-lm modules (e.g. %s); keeping the "
101
- "multimodal key namespace unchanged",
102
- non_lm[0],
103
- )
104
- return False
130
+ data_start = 8 + header_length
131
+ for key, descriptor in header.items():
132
+ if key == "__metadata__" or not _references_non_lm_modules(key):
133
+ continue
134
+ if _non_lm_tensor_is_live(
135
+ source,
136
+ key,
137
+ descriptor,
138
+ data_start=data_start,
139
+ file_size=file_size,
140
+ ):
141
+ logger.info(
142
+ "exported adapter has live non-lm weights (e.g. %s); keeping the "
143
+ "multimodal key namespace unchanged",
144
+ key,
145
+ )
146
+ return False
105
147
 
106
148
  normalized: dict[str, object] = {}
107
149
  remapped = 0
@@ -124,6 +166,7 @@ def _normalize_export_adapter_keys(adapter_dir: Path) -> bool:
124
166
  separators=(",", ":"),
125
167
  ).encode("utf-8")
126
168
  new_header += b" " * (-len(new_header) % 8)
169
+ source.seek(data_start)
127
170
  with tempfile.NamedTemporaryFile(
128
171
  mode="wb",
129
172
  dir=path.parent,
@@ -29,16 +29,47 @@ def require_key(authorization: str | None = Header(default=None)) -> dict:
29
29
  return key
30
30
 
31
31
 
32
- def owned_run(run_id: str, key: dict):
33
- """Load a run's status iff `key` owns it; 404 otherwise (don't leak existence)."""
34
- if db.run_owner(run_id) != key["id"]:
35
- raise HTTPException(status_code=404, detail=f"unknown run_id: {run_id}")
32
+ def _load_status(run_id: str):
33
+ """Resolve `run_id` to its status via the module (so `app.get_status` patches are honored)."""
36
34
  try:
37
35
  return _app.get_status(run_id)
38
36
  except FileNotFoundError as exc:
39
37
  raise HTTPException(status_code=404, detail=str(exc)) from exc
40
38
 
41
39
 
40
+ def owned_run(run_id: str, key: dict):
41
+ """Load a run's status iff `key` owns it; 404 otherwise (don't leak existence)."""
42
+ if db.run_owner(run_id) != key["id"]:
43
+ raise HTTPException(status_code=404, detail=f"unknown run_id: {run_id}")
44
+ return _load_status(run_id)
45
+
46
+
47
+ def readable_run(run_id: str, key: dict, org_id: str | None = None):
48
+ """Load a run's status for READ-only endpoints (`/logs`, `/worker`) via one of two paths.
49
+
50
+ Mirrors the env-delete posture (a user key carries its own identity; the org-agnostic
51
+ internal key is the trusted platform caller):
52
+ * exact-key owner -- the API key that created the run. This is the `flash log` CLI path.
53
+ * internal/service key + matching ``X-Freesolo-Org-Id`` -- the platform web proxy. The
54
+ browser has no per-run API key, so the platform authenticates the user, checks org
55
+ membership on the mirror row, then calls with the internal key and the run's org here.
56
+ The header is honored only for the internal key and only when it matches the run's
57
+ PERSISTED org, so a proxy bug can never cross orgs. A non-owner user key does reach this
58
+ check but fails the ``auth_kind == "internal"`` gate, so its org header is never honored.
59
+ 404 (never 403) on every failure so we never leak whether a run exists.
60
+ """
61
+ if db.run_owner(run_id) == key["id"]:
62
+ return _load_status(run_id)
63
+ org = (org_id or "").strip()
64
+ if key.get("auth_kind") == "internal" and org:
65
+ from flash.runner import _status_org_id
66
+
67
+ status = _load_status(run_id)
68
+ if _status_org_id(status) == org:
69
+ return status
70
+ raise HTTPException(status_code=404, detail=f"unknown run_id: {run_id}")
71
+
72
+
42
73
  def _require_bool(payload: dict, field: str, default: bool) -> bool:
43
74
  """Read ``payload[field]`` as a real JSON boolean; 400 on non-bool. Never coerce — "false" (str) is truthy."""
44
75
  value = payload.get(field, default)
@@ -7,7 +7,7 @@ import logging
7
7
  import os
8
8
  from typing import Annotated
9
9
 
10
- from fastapi import APIRouter, Depends, HTTPException
10
+ from fastapi import APIRouter, Depends, Header, HTTPException
11
11
 
12
12
  from flash.runner import (
13
13
  DeploymentRevocationError,
@@ -19,7 +19,14 @@ from flash.runner import (
19
19
  from flash.schema import train_schema_metadata
20
20
  from flash.server import app as _app
21
21
  from flash.server import db
22
- from flash.server._deps import _parse_spec, _require_bool, _runtime_secrets, owned_run, require_key
22
+ from flash.server._deps import (
23
+ _parse_spec,
24
+ _require_bool,
25
+ _runtime_secrets,
26
+ owned_run,
27
+ readable_run,
28
+ require_key,
29
+ )
23
30
  from flash.spec import JobSpec
24
31
 
25
32
  _LOG = logging.getLogger("flash.server.runs")
@@ -255,8 +262,13 @@ def run_status(run_id: str, key: Annotated[dict, Depends(require_key)]):
255
262
 
256
263
 
257
264
  @router.get("/v1/runs/{run_id}/logs")
258
- def run_logs(run_id: str, key: Annotated[dict, Depends(require_key)], offset: int = 0):
259
- status = owned_run(run_id, key)
265
+ def run_logs(
266
+ run_id: str,
267
+ key: Annotated[dict, Depends(require_key)],
268
+ offset: int = 0,
269
+ x_freesolo_org_id: Annotated[str | None, Header()] = None,
270
+ ):
271
+ status = readable_run(run_id, key, x_freesolo_org_id)
260
272
  log_path = runs_file_path(run_id, ".log")
261
273
  chunk, end = "", max(0, offset)
262
274
  if os.path.exists(log_path):
@@ -280,8 +292,12 @@ def run_logs(run_id: str, key: Annotated[dict, Depends(require_key)], offset: in
280
292
 
281
293
 
282
294
  @router.get("/v1/runs/{run_id}/worker")
283
- def run_worker_output(run_id: str, key: Annotated[dict, Depends(require_key)]):
284
- status = owned_run(run_id, key)
295
+ def run_worker_output(
296
+ run_id: str,
297
+ key: Annotated[dict, Depends(require_key)],
298
+ x_freesolo_org_id: Annotated[str | None, Header()] = None,
299
+ ):
300
+ status = readable_run(run_id, key, x_freesolo_org_id)
285
301
  return {"run_id": run_id, "worker": _app._worker_artifacts(JobSpec.from_dict(status.spec))}
286
302
 
287
303
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "freesolo-flash-dev"
3
- version = "1.0.1"
3
+ version = "1.0.2"
4
4
  description = "Flash — managed LoRA post-training (SFT/GRPO/OPD) for Freesolo environments, driven by the `flash` CLI"
5
5
  readme = "README.md"
6
6
  # RunPod Flash supports Python 3.11-3.12 (not 3.13 yet).
@@ -113,7 +113,7 @@ artifacts = ["flash/py.typed"]
113
113
  # auto-published by .github/workflows/publish-dev.yml: a push to `dev` whose version isn't yet on
114
114
  # PyPI publishes it; pushes that don't change it find it already published and no-op. So cutting a
115
115
  # dev-channel release = bump this (and [project].version) and merge to `dev`.
116
- version = "1.0.1"
116
+ version = "1.0.2"
117
117
 
118
118
  [tool.pytest.ini_options]
119
119
  pythonpath = ["."]
@@ -305,18 +305,52 @@ def test_export_adapter_key_collision_leaves_safetensors_unchanged(monkeypatch):
305
305
  assert uploaded["weights"] == source_bytes
306
306
 
307
307
 
308
+ def test_export_adapter_normalizes_lm_keys_with_inert_vision_weights(tmp_path):
309
+ from flash.serve import export
310
+
311
+ lm_a = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_A.default.weight"
312
+ lm_b = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_B.default.weight"
313
+ vision_a = "base_model.model.model.visual.blocks.0.attn.qkv.lora_A.default.weight"
314
+ vision_b = "base_model.model.model.visual.blocks.0.attn.qkv.lora_B.default.weight"
315
+ header = {
316
+ lm_a: {"dtype": "F16", "shape": [1], "data_offsets": [0, 2]},
317
+ lm_b: {"dtype": "F16", "shape": [1], "data_offsets": [2, 4]},
318
+ vision_a: {"dtype": "F16", "shape": [1], "data_offsets": [4, 6]},
319
+ vision_b: {"dtype": "F16", "shape": [1], "data_offsets": [6, 8]},
320
+ }
321
+ data = b"\x01\x02\x03\x04\x05\x06\x00\x00"
322
+ path = tmp_path / "adapter_model.safetensors"
323
+ path.write_bytes(_safetensors_bytes(header, data))
324
+
325
+ assert export._normalize_export_adapter_keys(tmp_path) is True
326
+
327
+ normalized_header, normalized_data = _parse_safetensors_bytes(path.read_bytes())
328
+ normalized_lm_a = lm_a.replace(".language_model.", ".", 1)
329
+ normalized_lm_b = lm_b.replace(".language_model.", ".", 1)
330
+ assert set(normalized_header) == {normalized_lm_a, normalized_lm_b, vision_a, vision_b}
331
+ assert normalized_header[normalized_lm_a] == header[lm_a]
332
+ assert normalized_header[normalized_lm_b] == header[lm_b]
333
+ assert normalized_header[vision_a] == header[vision_a]
334
+ assert normalized_header[vision_b] == header[vision_b]
335
+ assert normalized_data == data
336
+
337
+
308
338
  def test_export_adapter_with_vision_keys_leaves_safetensors_unchanged(monkeypatch):
309
- # a genuinely multimodal adapter (lora on the vision tower) must not be normalized:
339
+ # a genuinely multimodal adapter with nonzero vision lora_b must not be normalized:
310
340
  # stripping only its lm keys would leave a mixed namespace no transformers class loads.
311
341
  uploaded: dict = {}
312
- lm_key = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_A.default.weight"
313
- vision_key = "base_model.model.model.visual.blocks.0.attn.proj.lora_A.default.weight"
342
+ lm_a = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_A.default.weight"
343
+ lm_b = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_B.default.weight"
344
+ vision_a = "base_model.model.model.visual.blocks.0.attn.proj.lora_A.default.weight"
345
+ vision_b = "base_model.model.model.visual.blocks.0.attn.proj.lora_B.default.weight"
314
346
  source_bytes = _safetensors_bytes(
315
347
  {
316
- lm_key: {"dtype": "F16", "shape": [1], "data_offsets": [0, 2]},
317
- vision_key: {"dtype": "F16", "shape": [1], "data_offsets": [2, 4]},
348
+ lm_a: {"dtype": "F16", "shape": [1], "data_offsets": [0, 2]},
349
+ lm_b: {"dtype": "F16", "shape": [1], "data_offsets": [2, 4]},
350
+ vision_a: {"dtype": "F16", "shape": [1], "data_offsets": [4, 6]},
351
+ vision_b: {"dtype": "F16", "shape": [1], "data_offsets": [6, 8]},
318
352
  },
319
- b"\x01\x02\x03\x04",
353
+ b"\x01\x02\x03\x04\x05\x06\x07\x08",
320
354
  )
321
355
 
322
356
  def fake_snapshot_download(*, local_dir, **kw):
@@ -357,6 +391,44 @@ def test_export_adapter_with_vision_keys_leaves_safetensors_unchanged(monkeypatc
357
391
  assert uploaded["weights"] == source_bytes
358
392
 
359
393
 
394
+ def test_export_adapter_with_out_of_bounds_non_lm_offsets_is_unchanged(tmp_path):
395
+ from flash.serve import export
396
+
397
+ lm_key = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_A.default.weight"
398
+ vision_b = "base_model.model.model.visual.blocks.0.attn.proj.lora_B.default.weight"
399
+ source_bytes = _safetensors_bytes(
400
+ {
401
+ lm_key: {"dtype": "F16", "shape": [1], "data_offsets": [0, 2]},
402
+ vision_b: {"dtype": "F16", "shape": [1], "data_offsets": [2, 5]},
403
+ },
404
+ b"\x01\x02\x00\x00",
405
+ )
406
+ path = tmp_path / "adapter_model.safetensors"
407
+ path.write_bytes(source_bytes)
408
+
409
+ assert export._normalize_export_adapter_keys(tmp_path) is False
410
+ assert path.read_bytes() == source_bytes
411
+
412
+
413
+ def test_export_adapter_with_unrecognized_non_lm_tensor_is_unchanged(tmp_path):
414
+ from flash.serve import export
415
+
416
+ lm_key = "base_model.model.model.language_model.layers.0.mlp.up_proj.lora_A.default.weight"
417
+ vision_saved = "base_model.model.model.visual.proj.modules_to_save.default.weight"
418
+ source_bytes = _safetensors_bytes(
419
+ {
420
+ lm_key: {"dtype": "F16", "shape": [1], "data_offsets": [0, 2]},
421
+ vision_saved: {"dtype": "F16", "shape": [1], "data_offsets": [2, 4]},
422
+ },
423
+ b"\x01\x02\x00\x00",
424
+ )
425
+ path = tmp_path / "adapter_model.safetensors"
426
+ path.write_bytes(source_bytes)
427
+
428
+ assert export._normalize_export_adapter_keys(tmp_path) is False
429
+ assert path.read_bytes() == source_bytes
430
+
431
+
360
432
  def test_export_adapter_without_safetensors_still_exports(monkeypatch):
361
433
  uploaded: dict = {}
362
434
  bin_weights = b"legacy-adapter-weights"
@@ -1145,6 +1145,115 @@ def test_worker_output_route(api, monkeypatch):
1145
1145
  assert api.get(f"/v1/runs/{run_id}/worker", headers=_bearer(other)).status_code == 404
1146
1146
 
1147
1147
 
1148
+ def test_internal_key_reads_logs_and_worker_with_matching_org(api, monkeypatch):
1149
+ # The platform web proxy has no per-run API key, so it reads a user's run logs with the shared
1150
+ # internal key plus the run's org in X-Freesolo-Org-Id. The header is honored ONLY for the
1151
+ # internal key and ONLY when it matches the run's persisted org. All failures are 404 so run
1152
+ # existence is never leaked, and a rejected request must not trigger the expensive worker fetch.
1153
+ import flash.server.app as app_mod
1154
+
1155
+ owner = _login()
1156
+ org = f"org-{owner.removeprefix(_USER_PREFIX)}"
1157
+ run_id = api.post(
1158
+ "/v1/runs", json={"spec": SPEC, "dry_run": True}, headers=_bearer(owner)
1159
+ ).json()["run_id"]
1160
+ with open(os.path.join(_orch.RUNS_DIR, f"{run_id}.log"), "w") as f:
1161
+ f.write("orchestrator line\n")
1162
+
1163
+ worker_calls = {"n": 0}
1164
+
1165
+ def counting_worker(spec):
1166
+ worker_calls["n"] += 1
1167
+ return {"console_sft.txt": "worker stdout\n"}
1168
+
1169
+ monkeypatch.setattr(app_mod, "_worker_artifacts", counting_worker)
1170
+
1171
+ internal = _bearer("fslo-internal-test")
1172
+ matched = {**internal, "X-Freesolo-Org-Id": org}
1173
+
1174
+ logs = api.get(f"/v1/runs/{run_id}/logs", headers=matched)
1175
+ assert logs.status_code == 200, logs.text
1176
+ assert logs.json()["logs"] == "orchestrator line\n"
1177
+ worker = api.get(f"/v1/runs/{run_id}/worker", headers=matched)
1178
+ assert worker.status_code == 200, worker.text
1179
+ assert worker.json()["worker"] == {"console_sft.txt": "worker stdout\n"}
1180
+ assert worker_calls["n"] == 1
1181
+
1182
+ # wrong org, empty/whitespace org, or no header at all -> 404, and none of these rejected
1183
+ # /worker requests reach the expensive artifact fetch (the call count stays at 1).
1184
+ for headers in (
1185
+ {**internal, "X-Freesolo-Org-Id": "org-someone-else"},
1186
+ {**internal, "X-Freesolo-Org-Id": ""},
1187
+ {**internal, "X-Freesolo-Org-Id": " "},
1188
+ internal,
1189
+ ):
1190
+ assert api.get(f"/v1/runs/{run_id}/logs", headers=headers).status_code == 404
1191
+ assert api.get(f"/v1/runs/{run_id}/worker", headers=headers).status_code == 404
1192
+ assert worker_calls["n"] == 1
1193
+
1194
+
1195
+ def test_org_header_is_ignored_for_non_internal_keys(api):
1196
+ # A non-owner user key can't read another tenant's logs even if it forges the org header: the
1197
+ # header is consulted only for the internal service key, so the owner check still governs users.
1198
+ owner = _login()
1199
+ org = f"org-{owner.removeprefix(_USER_PREFIX)}"
1200
+ run_id = api.post(
1201
+ "/v1/runs", json={"spec": SPEC, "dry_run": True}, headers=_bearer(owner)
1202
+ ).json()["run_id"]
1203
+
1204
+ intruder = _login()
1205
+ forged = {**_bearer(intruder), "X-Freesolo-Org-Id": org}
1206
+ assert api.get(f"/v1/runs/{run_id}/logs", headers=forged).status_code == 404
1207
+ assert api.get(f"/v1/runs/{run_id}/worker", headers=forged).status_code == 404
1208
+
1209
+ # the real owner still reads its own run with no org header (unchanged CLI path).
1210
+ assert api.get(f"/v1/runs/{run_id}/logs", headers=_bearer(owner)).status_code == 200
1211
+
1212
+
1213
+ def test_internal_org_header_does_not_widen_owner_only_endpoints(api):
1214
+ # readable_run only governs the read-only /logs and /worker endpoints. /status, /cancel, and
1215
+ # /checkpoints stay owner-only via owned_run, so the internal key plus a matching org header
1216
+ # must NOT grant access to them.
1217
+ owner = _login()
1218
+ org = f"org-{owner.removeprefix(_USER_PREFIX)}"
1219
+ run_id = api.post(
1220
+ "/v1/runs", json={"spec": SPEC, "dry_run": True}, headers=_bearer(owner)
1221
+ ).json()["run_id"]
1222
+
1223
+ matched = {**_bearer("fslo-internal-test"), "X-Freesolo-Org-Id": org}
1224
+ assert api.get(f"/v1/runs/{run_id}", headers=matched).status_code == 404
1225
+ assert api.post(f"/v1/runs/{run_id}/cancel", headers=matched).status_code == 404
1226
+ assert api.get(f"/v1/runs/{run_id}/checkpoints", headers=matched).status_code == 404
1227
+
1228
+
1229
+ def test_internal_key_cannot_read_run_without_persisted_org(api, monkeypatch):
1230
+ # A run with no persisted org context cannot be matched by the internal path even with a
1231
+ # non-empty header: _status_org_id is empty, so it never equals a real org and fails closed.
1232
+ import flash.server.app as app_mod
1233
+
1234
+ owner = _login()
1235
+ run_id = api.post(
1236
+ "/v1/runs", json={"spec": SPEC, "dry_run": True}, headers=_bearer(owner)
1237
+ ).json()["run_id"]
1238
+
1239
+ real_get_status = app_mod.get_status
1240
+
1241
+ def stripped(rid):
1242
+ status = real_get_status(rid)
1243
+ if rid == run_id:
1244
+ status.platform_context = None
1245
+ status.billing_context = None
1246
+ return status
1247
+
1248
+ monkeypatch.setattr(app_mod, "get_status", stripped)
1249
+
1250
+ internal = _bearer("fslo-internal-test")
1251
+ for org in ("org-anything", f"org-{owner.removeprefix(_USER_PREFIX)}"):
1252
+ headers = {**internal, "X-Freesolo-Org-Id": org}
1253
+ assert api.get(f"/v1/runs/{run_id}/logs", headers=headers).status_code == 404
1254
+ assert api.get(f"/v1/runs/{run_id}/worker", headers=headers).status_code == 404
1255
+
1256
+
1148
1257
  def test_latest_error_artifact_name_picks_highest_attempt(monkeypatch):
1149
1258
  """The logs fetcher resolves the newest attempt-scoped error file, so a retried-then-failed run
1150
1259
  surfaces the FINAL attempt's traceback, not attempt0's stale one."""
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import multiprocessing
5
+ import subprocess
6
+ import sys
5
7
  from pathlib import Path
6
8
 
7
9
  import pytest
@@ -199,3 +201,30 @@ def test_verified_revision_ledger_survives_new_process(monkeypatch, tmp_path):
199
201
 
200
202
  assert process.exitcode == 0
201
203
  assert output.get(timeout=2) == (revision,)
204
+
205
+
206
+ def test_verified_revision_ledger_fails_closed_without_fcntl(monkeypatch):
207
+ # fcntl is unix-only; on platforms without it the ledger must fail closed
208
+ # rather than silently skip its cross-process lock.
209
+ from flash.runner import verified_revisions
210
+
211
+ monkeypatch.setattr(verified_revisions, "fcntl", None)
212
+ with pytest.raises(RuntimeError, match="verified-revision locking is unavailable"):
213
+ verified_revisions.read_verified_adapter_revisions("run-without-fcntl")
214
+
215
+
216
+ def test_cli_import_chain_survives_missing_fcntl():
217
+ # reproduces the windows `flash login` crash: fcntl is unix-only, so the cli
218
+ # import chain (flash.cli -> flash.runner -> verified_revisions) must not
219
+ # hard-depend on it. blocking fcntl mimics a platform that lacks the module.
220
+ code = (
221
+ "import sys; sys.modules['fcntl'] = None; "
222
+ "import flash.runner.verified_revisions; "
223
+ "import flash.cli"
224
+ )
225
+ result = subprocess.run(
226
+ [sys.executable, "-c", code],
227
+ capture_output=True,
228
+ text=True,
229
+ )
230
+ assert result.returncode == 0, result.stderr
@@ -1024,7 +1024,7 @@ wheels = [
1024
1024
 
1025
1025
  [[package]]
1026
1026
  name = "freesolo-flash"
1027
- version = "1.0.1"
1027
+ version = "1.0.2"
1028
1028
  source = { editable = "." }
1029
1029
 
1030
1030
  [package.optional-dependencies]