openenv 0.4.2__tar.gz → 0.6.0__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 (182) hide show
  1. {openenv-0.4.2/src/openenv.egg-info → openenv-0.6.0}/PKG-INFO +11 -3
  2. {openenv-0.4.2 → openenv-0.6.0}/README.md +5 -2
  3. {openenv-0.4.2 → openenv-0.6.0}/pyproject.toml +19 -1
  4. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/auto/_discovery.py +159 -9
  5. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/__main__.py +12 -3
  6. openenv-0.6.0/src/openenv/cli/commands/catalog.py +121 -0
  7. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/collect.py +42 -27
  8. openenv-0.6.0/src/openenv/cli/commands/harbor.py +715 -0
  9. openenv-0.6.0/src/openenv/cli/commands/validate.py +247 -0
  10. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/README.md +8 -2
  11. openenv-0.6.0/src/openenv/cli/templates/openenv_env/openenv.yaml +23 -0
  12. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +2 -2
  13. openenv-0.6.0/src/openenv/core/containers/runtime/novita_provider.py +1207 -0
  14. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_client.py +256 -62
  15. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/http_server.py +99 -16
  16. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/serialization.py +13 -3
  17. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/web_interface.py +4 -0
  18. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/harness/__init__.py +101 -1
  19. openenv-0.6.0/src/openenv/core/harness/capture/__init__.py +37 -0
  20. openenv-0.6.0/src/openenv/core/harness/capture/compat.py +287 -0
  21. openenv-0.6.0/src/openenv/core/harness/capture/contract.py +331 -0
  22. openenv-0.6.0/src/openenv/core/harness/capture/detection.py +65 -0
  23. openenv-0.6.0/src/openenv/core/harness/capture/dialects/__init__.py +23 -0
  24. openenv-0.6.0/src/openenv/core/harness/capture/dialects/anthropic.py +705 -0
  25. openenv-0.6.0/src/openenv/core/harness/capture/dialects/base.py +134 -0
  26. openenv-0.6.0/src/openenv/core/harness/capture/dialects/google.py +729 -0
  27. openenv-0.6.0/src/openenv/core/harness/capture/dialects/images.py +335 -0
  28. openenv-0.6.0/src/openenv/core/harness/capture/dialects/openai_chat.py +41 -0
  29. openenv-0.6.0/src/openenv/core/harness/capture/dialects/openai_responses.py +1125 -0
  30. openenv-0.6.0/src/openenv/core/harness/capture/dialects/reasoning.py +125 -0
  31. openenv-0.6.0/src/openenv/core/harness/capture/export.py +246 -0
  32. openenv-0.6.0/src/openenv/core/harness/capture/forwarding.py +346 -0
  33. openenv-0.6.0/src/openenv/core/harness/capture/graph.py +496 -0
  34. openenv-0.6.0/src/openenv/core/harness/capture/providers.py +325 -0
  35. openenv-0.6.0/src/openenv/core/harness/capture/runner.py +179 -0
  36. openenv-0.6.0/src/openenv/core/harness/capture/server.py +1474 -0
  37. openenv-0.6.0/src/openenv/core/harness/capture/sessions.py +280 -0
  38. openenv-0.6.0/src/openenv/core/harness/capture/sse.py +255 -0
  39. openenv-0.6.0/src/openenv/core/harness/capture/upstream.py +603 -0
  40. openenv-0.6.0/src/openenv/core/harness/capture/validate.py +455 -0
  41. openenv-0.6.0/src/openenv/core/harness/capture/validate_llm.py +768 -0
  42. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/harness/collect.py +57 -14
  43. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/llm_client.py +95 -15
  44. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/mcp_client.py +93 -27
  45. openenv-0.6.0/src/openenv/discovery/__init__.py +34 -0
  46. openenv-0.6.0/src/openenv/discovery/errors.py +5 -0
  47. openenv-0.6.0/src/openenv/discovery/metadata.py +191 -0
  48. openenv-0.6.0/src/openenv/discovery/models.py +425 -0
  49. openenv-0.6.0/src/openenv/discovery/producer.py +309 -0
  50. openenv-0.6.0/src/openenv/discovery/repository.py +148 -0
  51. openenv-0.6.0/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json +715 -0
  52. openenv-0.6.0/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json +171 -0
  53. openenv-0.6.0/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json +396 -0
  54. openenv-0.6.0/src/openenv/discovery/search.py +131 -0
  55. openenv-0.6.0/src/openenv/discovery/serialization.py +127 -0
  56. openenv-0.6.0/src/openenv/harbor/__init__.py +18 -0
  57. openenv-0.6.0/src/openenv/harbor/atif.py +563 -0
  58. openenv-0.6.0/src/openenv/harbor/capabilities.py +316 -0
  59. openenv-0.6.0/src/openenv/harbor/client.py +216 -0
  60. openenv-0.6.0/src/openenv/harbor/contract.py +193 -0
  61. openenv-0.6.0/src/openenv/harbor/e2b_stream.py +64 -0
  62. openenv-0.6.0/src/openenv/harbor/environment.py +338 -0
  63. openenv-0.6.0/src/openenv/harbor/install_fixes.py +698 -0
  64. openenv-0.6.0/src/openenv/harbor/models.py +352 -0
  65. openenv-0.6.0/src/openenv/harbor/nemo_profile.py +26 -0
  66. openenv-0.6.0/src/openenv/harbor/proc_env_context.py +133 -0
  67. openenv-0.6.0/src/openenv/harbor/qualification.py +154 -0
  68. openenv-0.6.0/src/openenv/harbor/rollout.py +777 -0
  69. openenv-0.6.0/src/openenv/harbor/runner.py +228 -0
  70. openenv-0.6.0/src/openenv/harbor/seams.py +966 -0
  71. openenv-0.6.0/src/openenv/harbor/serving.py +324 -0
  72. openenv-0.6.0/src/openenv/harbor/shared_template.py +106 -0
  73. openenv-0.6.0/src/openenv/harbor/startup.py +212 -0
  74. openenv-0.6.0/src/openenv/harbor/tasks.py +302 -0
  75. openenv-0.6.0/src/openenv/harbor/ui.py +1511 -0
  76. openenv-0.6.0/src/openenv/validation/__init__.py +94 -0
  77. openenv-0.6.0/src/openenv/validation/graders/__init__.py +165 -0
  78. openenv-0.6.0/src/openenv/validation/graders/static/__init__.py +5 -0
  79. openenv-0.6.0/src/openenv/validation/graders/static/manifest.py +72 -0
  80. openenv-0.6.0/src/openenv/validation/manifest.py +386 -0
  81. openenv-0.6.0/src/openenv/validation/parsers/__init__.py +69 -0
  82. openenv-0.6.0/src/openenv/validation/parsers/openenv_yaml.py +101 -0
  83. openenv-0.6.0/src/openenv/validation/policies/severity-v1.json +56 -0
  84. openenv-0.6.0/src/openenv/validation/policies/severity-v2.json +57 -0
  85. openenv-0.6.0/src/openenv/validation/policy.py +149 -0
  86. openenv-0.6.0/src/openenv/validation/providers/__init__.py +79 -0
  87. openenv-0.6.0/src/openenv/validation/providers/docker.py +510 -0
  88. openenv-0.6.0/src/openenv/validation/report.py +103 -0
  89. openenv-0.6.0/src/openenv/validation/runner.py +157 -0
  90. openenv-0.6.0/src/openenv/validation/runtime/__init__.py +1 -0
  91. openenv-0.6.0/src/openenv/validation/runtime/contracts.py +238 -0
  92. openenv-0.6.0/src/openenv/validation/schemas/manifest-v2.schema.json +469 -0
  93. openenv-0.6.0/src/openenv/validation/schemas/manifest.schema.json +430 -0
  94. openenv-0.6.0/src/openenv/validation/schemas/report-v2.schema.json +712 -0
  95. openenv-0.6.0/src/openenv/validation/schemas/report.schema.json +591 -0
  96. openenv-0.6.0/src/openenv/validation/schemas/runtime-plan.schema.json +62 -0
  97. openenv-0.6.0/src/openenv/validation/signature.py +120 -0
  98. openenv-0.6.0/src/openenv/validation/types.py +88 -0
  99. {openenv-0.4.2 → openenv-0.6.0/src/openenv.egg-info}/PKG-INFO +11 -3
  100. {openenv-0.4.2 → openenv-0.6.0}/src/openenv.egg-info/SOURCES.txt +80 -0
  101. {openenv-0.4.2 → openenv-0.6.0}/src/openenv.egg-info/requires.txt +9 -0
  102. openenv-0.4.2/src/openenv/cli/commands/validate.py +0 -197
  103. openenv-0.4.2/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -7
  104. {openenv-0.4.2 → openenv-0.6.0}/LICENSE +0 -0
  105. {openenv-0.4.2 → openenv-0.6.0}/MANIFEST.in +0 -0
  106. {openenv-0.4.2 → openenv-0.6.0}/setup.cfg +0 -0
  107. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/__init__.py +0 -0
  108. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/auto/__init__.py +0 -0
  109. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/auto/auto_action.py +0 -0
  110. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/auto/auto_env.py +0 -0
  111. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/__init__.py +0 -0
  112. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/_cli_utils.py +0 -0
  113. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/_validation.py +0 -0
  114. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/__init__.py +0 -0
  115. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/build.py +0 -0
  116. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/fork.py +0 -0
  117. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/import_env.py +0 -0
  118. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/init.py +0 -0
  119. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/push.py +0 -0
  120. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/serve.py +0 -0
  121. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/commands/skills.py +0 -0
  122. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/__init__.py +0 -0
  123. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/base.py +0 -0
  124. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/ors.py +0 -0
  125. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/templates/__init__.py +0 -0
  126. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/templates/ors_app.py.tpl +0 -0
  127. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/templates/ors_environment.py.tpl +0 -0
  128. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/templates/verifiers_app.py.tpl +0 -0
  129. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +0 -0
  130. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/importers/verifiers.py +0 -0
  131. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/__init__.py +0 -0
  132. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
  133. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/client.py +0 -0
  134. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/models.py +0 -0
  135. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
  136. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
  137. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
  138. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
  139. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
  140. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/__init__.py +0 -0
  141. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/client_types.py +0 -0
  142. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/__init__.py +0 -0
  143. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/__init__.py +0 -0
  144. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/_server_config.py +0 -0
  145. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
  146. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/daytona_provider.py +0 -0
  147. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
  148. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/modal_provider.py +0 -0
  149. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/providers.py +0 -0
  150. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
  151. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
  152. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/__init__.py +0 -0
  153. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/_utils.py +0 -0
  154. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/base_transforms.py +0 -0
  155. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/exceptions.py +0 -0
  156. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/gradio_theme.py +0 -0
  157. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/gradio_ui.py +0 -0
  158. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/interfaces.py +0 -0
  159. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/mcp_environment.py +0 -0
  160. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/mcp_types.py +0 -0
  161. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/route_config.py +0 -0
  162. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/env_server/types.py +0 -0
  163. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/evals/__init__.py +0 -0
  164. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/evals/base.py +0 -0
  165. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/evals/inspect_harness.py +0 -0
  166. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/evals/types.py +0 -0
  167. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/generic_client.py +0 -0
  168. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/rubrics/__init__.py +0 -0
  169. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/rubrics/base.py +0 -0
  170. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/rubrics/containers.py +0 -0
  171. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/rubrics/llm_judge.py +0 -0
  172. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/rubrics/trajectory.py +0 -0
  173. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/sync_client.py +0 -0
  174. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/tools/__init__.py +0 -0
  175. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/tools/git_server_client.py +0 -0
  176. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/tools/local_python_executor.py +0 -0
  177. {openenv-0.4.2 → openenv-0.6.0}/src/openenv/core/utils.py +0 -0
  178. {openenv-0.4.2 → openenv-0.6.0}/src/openenv.egg-info/dependency_links.txt +0 -0
  179. {openenv-0.4.2 → openenv-0.6.0}/src/openenv.egg-info/entry_points.txt +0 -0
  180. {openenv-0.4.2 → openenv-0.6.0}/src/openenv.egg-info/top_level.txt +0 -0
  181. {openenv-0.4.2 → openenv-0.6.0}/src/openenv_core/__init__.py +0 -0
  182. {openenv-0.4.2 → openenv-0.6.0}/tests/test_line_endings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openenv
3
- Version: 0.4.2
3
+ Version: 0.6.0
4
4
  Summary: A unified framework for reinforcement learning environments
5
5
  License-Expression: BSD-3-Clause
6
6
  Requires-Python: >=3.10
@@ -8,6 +8,7 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: fastapi>=0.116.0
10
10
  Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: packaging>=24.2
11
12
  Requires-Dist: typing_extensions>=4.0.0
12
13
  Requires-Dist: uvicorn>=0.24.0
13
14
  Requires-Dist: requests>=2.25.0
@@ -31,8 +32,12 @@ Requires-Dist: azure-identity>=1.23.0; extra == "aca"
31
32
  Provides-Extra: modal
32
33
  Requires-Dist: modal>=1.4.0; extra == "modal"
33
34
  Requires-Dist: pyyaml>=6.0; extra == "modal"
35
+ Provides-Extra: novita
36
+ Requires-Dist: novita-sandbox>=2.1.1; extra == "novita"
34
37
  Provides-Extra: inspect
35
38
  Requires-Dist: inspect-ai>=0.3.0; extra == "inspect"
39
+ Provides-Extra: harbor
40
+ Requires-Dist: harbor[beam,blaxel,cua,cwsandbox,daytona,e2b,ec2,gke,islo,modal,novita,opensandbox,runloop,use-computer]>=0.22.0; python_version >= "3.12" and extra == "harbor"
36
41
  Dynamic: license-file
37
42
 
38
43
  # OpenEnv: Agentic Execution Environments
@@ -395,6 +400,9 @@ See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%
395
400
  ### Lightning AI
396
401
  [Lightning AI templates](https://lightning.ai/templates?section=featured&query=openenv)
397
402
 
403
+ ### Miles
404
+ See the [Terminal-Bench-2 GRPO example](https://github.com/radixark/miles/tree/main/examples/experimental/openenv) on how to train on OpenEnv environments with Miles.
405
+
398
406
  ## Example Environments
399
407
 
400
408
  | Environment | Description |
@@ -409,11 +417,11 @@ See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%
409
417
 
410
418
  ## Community Support & Acknowledgments
411
419
 
412
- OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, and RadixArk.
420
+ OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, RadixArk, and Nebius.
413
421
 
414
422
  The project is also supported by a broader community of organizations. If you would like to add your project or organization here, please open a pull request for maintainer review.
415
423
 
416
- Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles)
424
+ Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles), [Nebius](https://nebius.com)
417
425
 
418
426
  And we'd also like to acknowledge the team at Farama Foundation as the OpenEnv API was heavily inspired by the work you all have done on Gymnasium. Cheers!
419
427
 
@@ -358,6 +358,9 @@ See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%
358
358
  ### Lightning AI
359
359
  [Lightning AI templates](https://lightning.ai/templates?section=featured&query=openenv)
360
360
 
361
+ ### Miles
362
+ See the [Terminal-Bench-2 GRPO example](https://github.com/radixark/miles/tree/main/examples/experimental/openenv) on how to train on OpenEnv environments with Miles.
363
+
361
364
  ## Example Environments
362
365
 
363
366
  | Environment | Description |
@@ -372,11 +375,11 @@ See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%
372
375
 
373
376
  ## Community Support & Acknowledgments
374
377
 
375
- OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, and RadixArk.
378
+ OpenEnv is governed by a technical committee that coordinates project direction, major technical decisions, RFCs, and release planning through the public issue tracker, pull requests, and RFC process. Current committee members: Meta-PyTorch, Reflection, Unsloth, Modal, Prime Intellect, Nvidia, Mercor, Fleet AI, Microsoft, Hugging Face, RadixArk, and Nebius.
376
379
 
377
380
  The project is also supported by a broader community of organizations. If you would like to add your project or organization here, please open a pull request for maintainer review.
378
381
 
379
- Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles)
382
+ Supporters include: [Meta-PyTorch](https://github.com/meta-pytorch), [Hugging Face](https://huggingface.co), [Scaler AI Labs](https://scalerailabs.com), [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), [Unsloth](https://unsloth.ai), [Reflection](https://reflection.ai), [vLLM](https://vllm.ai), [SkyRL](https://skyrl.readthedocs.io) (UC-Berkeley), [Lightning AI](https://lightning.ai), [Axolotl AI](https://github.com/axolotl-ai-cloud/axolotl), [Stanford Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/), [Mithril](https://mithril.ai), [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/), [Turing](https://www.turing.com/), [Scale AI](https://scale.com/), [Scorecard](https://www.scorecard.io/), [Snorkel AI](https://snorkel.ai/), [SGLang](https://github.com/sgl-project/sglang), [Miles](https://github.com/radixark/miles), [Nebius](https://nebius.com)
380
383
 
381
384
  And we'd also like to acknowledge the team at Farama Foundation as the OpenEnv API was heavily inspired by the work you all have done on Gymnasium. Cheers!
382
385
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openenv"
7
- version = "0.4.2"
7
+ version = "0.6.0"
8
8
  description = "A unified framework for reinforcement learning environments"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -16,6 +16,7 @@ dependencies = [
16
16
  # individual environment pyproject.toml files
17
17
  "fastapi>=0.116.0",
18
18
  "pydantic>=2.0.0",
19
+ "packaging>=24.2",
19
20
  "typing_extensions>=4.0.0",
20
21
  "uvicorn>=0.24.0",
21
22
  "requests>=2.25.0",
@@ -52,9 +53,23 @@ modal = [
52
53
  "modal>=1.4.0",
53
54
  "pyyaml>=6.0",
54
55
  ]
56
+ novita = [
57
+ "novita-sandbox>=2.1.1",
58
+ ]
55
59
  inspect = [
56
60
  "inspect-ai>=0.3.0",
57
61
  ]
62
+ harbor = [
63
+ # Every sandbox backend. Bare `harbor` gives one that lists all 23 backends and can
64
+ # instantiate none of them: each raises MissingExtraError from its constructor, which
65
+ # surfaces as a failed rollout rather than as a missing dependency.
66
+ #
67
+ # Not `harbor[cloud]`, which is unsatisfiable: it pulls `langsmith[sandbox]`
68
+ # (websockets>=15) and `tensorlake` (websockets>=13,<14) together.
69
+ #
70
+ # Harbor requires Python >= 3.12, so this extra is excluded from the 3.11 CI leg.
71
+ "harbor[e2b,modal,daytona,gke,ec2,runloop,novita,blaxel,beam,islo,opensandbox,cwsandbox,use-computer,cua]>=0.22.0; python_version >= '3.12'",
72
+ ]
58
73
 
59
74
  [project.scripts]
60
75
  openenv = "openenv.cli.__main__:main"
@@ -66,6 +81,8 @@ include-package-data = true
66
81
  [tool.setuptools.package-data]
67
82
  "openenv.cli" = ["templates/**/*"]
68
83
  "openenv.cli.importers" = ["templates/**/*"]
84
+ "openenv.validation" = ["policies/*.json", "schemas/*.json"]
85
+ "openenv.discovery" = ["schemas/**/*.json"]
69
86
 
70
87
  [tool.setuptools.exclude-package-data]
71
88
  "*" = ["*.pyc", "*.pyo", "__pycache__/*"]
@@ -106,6 +123,7 @@ dev = [
106
123
  "usort>=1.1.0",
107
124
  "pytest>=7.0",
108
125
  "pytest-asyncio>=0.21",
126
+ "jsonschema>=4.20.0",
109
127
  ]
110
128
 
111
129
  [tool.usort]
@@ -20,8 +20,9 @@ import importlib.metadata
20
20
  import importlib.resources
21
21
  import json
22
22
  import logging
23
+ import os
23
24
  import re
24
- import tempfile
25
+ import stat
25
26
  from dataclasses import asdict, dataclass
26
27
  from pathlib import Path
27
28
  from typing import Any, Type
@@ -336,6 +337,118 @@ def _create_env_info_from_package(
336
337
  )
337
338
 
338
339
 
340
+ def _default_cache_file() -> Path:
341
+ """
342
+ Return the per-user discovery cache file path.
343
+
344
+ Uses a per-user cache directory (``$XDG_CACHE_HOME`` or ``~/.cache``) rather than a
345
+ shared, world-writable temporary directory. A fixed path under the shared temp dir
346
+ lets another local user pre-create the cache file and redirect discovery to
347
+ attacker-controlled import paths (`import_module` on a cached `client_module_path`).
348
+ Per the XDG Base Directory specification, relative `XDG_CACHE_HOME` values are
349
+ ignored so an untrusted working tree cannot supply a victim-owned cache file.
350
+ The fallback home must itself be absolute; otherwise discovery fails closed.
351
+ """
352
+ base = os.environ.get("XDG_CACHE_HOME")
353
+ if base and Path(base).is_absolute():
354
+ root = Path(base)
355
+ else:
356
+ home = Path.home()
357
+ if not home.is_absolute():
358
+ raise RuntimeError(
359
+ "discovery cache requires an absolute home directory when "
360
+ "XDG_CACHE_HOME is unset or relative"
361
+ )
362
+ root = home / ".cache"
363
+ return root / "openenv" / "discovery_cache.json"
364
+
365
+
366
+ def _is_trusted_stat(info: os.stat_result) -> bool:
367
+ """
368
+ Return whether the file *info* describes is safe to load.
369
+
370
+ On POSIX the file must be owned by the current user and not writable by group or
371
+ others, so a cache file planted by another user is ignored rather than trusted.
372
+
373
+ Args:
374
+ info (`os.stat_result`):
375
+ Metadata for the file being considered.
376
+
377
+ Returns:
378
+ `bool`: `True` if the file is owned by the current user and not
379
+ group/other-writable (always `True` on non-POSIX platforms).
380
+ """
381
+ if os.name != "posix":
382
+ return True
383
+ return info.st_uid == os.getuid() and not (
384
+ info.st_mode & (stat.S_IWGRP | stat.S_IWOTH)
385
+ )
386
+
387
+
388
+ def _is_trusted_cache_file(path: Path) -> bool:
389
+ """
390
+ Return whether *path* is safe to load.
391
+
392
+ Prefer `_open_trusted_cache`, which checks the descriptor it hands back. This
393
+ variant resolves the path a second time, so on its own it cannot promise that
394
+ the file inspected is the file later read.
395
+
396
+ Args:
397
+ path (`Path`):
398
+ The cache file to check.
399
+
400
+ Returns:
401
+ `bool`: `True` if the file is owned by the current user and not
402
+ group/other-writable (always `True` on non-POSIX platforms).
403
+ """
404
+ if os.name != "posix":
405
+ return True
406
+ try:
407
+ return _is_trusted_stat(path.stat())
408
+ except OSError:
409
+ return False
410
+
411
+
412
+ def _open_trusted_cache(path: Path) -> int | None:
413
+ """
414
+ Open *path* for reading and return the descriptor only if it is trustworthy.
415
+
416
+ Checking a path and then opening it are two separate resolutions, and an
417
+ attacker who can write in the cache directory can swap the file for a symlink
418
+ in between, so the file that was vetted is not the file that gets read.
419
+ ``O_NOFOLLOW`` refuses a symlink outright and ``fstat`` inspects the descriptor
420
+ itself, which makes the ownership check and the read the same object.
421
+
422
+ Args:
423
+ path (`Path`):
424
+ The cache file to open.
425
+
426
+ Returns:
427
+ `int` or `None`: an open read-only descriptor the caller must close, or
428
+ `None` if the file is missing, is a symlink, is not a regular file, or
429
+ is not owned by the current user.
430
+ """
431
+ # O_NONBLOCK matters before the descriptor can be inspected: opening a FIFO
432
+ # read-only otherwise waits for a writer that an attacker simply never
433
+ # provides, so discovery hangs before any check runs. It has no effect on a
434
+ # regular file, which is the only thing accepted here anyway.
435
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
436
+ try:
437
+ fd = os.open(path, flags)
438
+ except OSError:
439
+ # Missing, unreadable, or a symlink (ELOOP under O_NOFOLLOW).
440
+ return None
441
+ try:
442
+ info = os.fstat(fd)
443
+ if not stat.S_ISREG(info.st_mode) or not _is_trusted_stat(info):
444
+ os.close(fd)
445
+ return None
446
+ except OSError:
447
+ os.close(fd)
448
+ return None
449
+ return fd
450
+
451
+
339
452
  class EnvironmentDiscovery:
340
453
  """
341
454
  Auto-discovery system for OpenEnv environments using installed packages.
@@ -346,7 +459,7 @@ class EnvironmentDiscovery:
346
459
  def __init__(self):
347
460
  """Initialize discovery system."""
348
461
  self._cache: dict[str, EnvironmentInfo] | None = None
349
- self._cache_file = Path(tempfile.gettempdir()) / "openenv_discovery_cache.json"
462
+ self._cache_file = _default_cache_file()
350
463
 
351
464
  def _discover_installed_packages(self) -> dict[str, EnvironmentInfo]:
352
465
  """
@@ -408,11 +521,22 @@ class EnvironmentDiscovery:
408
521
  Returns:
409
522
  Dictionary of env_key -> EnvironmentInfo, or None if cache invalid
410
523
  """
411
- if not self._cache_file.exists():
524
+ # Only trust a cache file owned by the current user. This prevents another
525
+ # local user from planting a file that would redirect discovery (and the
526
+ # subsequent import_module) to attacker-controlled modules/classes. The
527
+ # descriptor is what gets vetted and then read, so the file cannot be
528
+ # swapped for a symlink after the check.
529
+ fd = _open_trusted_cache(self._cache_file)
530
+ if fd is None:
531
+ if self._cache_file.exists():
532
+ logger.warning(
533
+ f"Ignoring discovery cache {self._cache_file}: not owned by "
534
+ "the current user, writable by group/others, or a symlink."
535
+ )
412
536
  return None
413
537
 
414
538
  try:
415
- with open(self._cache_file, "r") as f:
539
+ with os.fdopen(fd, "r") as f:
416
540
  cache_data = json.load(f)
417
541
 
418
542
  # Reconstruct EnvironmentInfo objects
@@ -437,8 +561,30 @@ class EnvironmentDiscovery:
437
561
  for env_key, env_info in environments.items():
438
562
  cache_data[env_key] = asdict(env_info)
439
563
 
440
- with open(self._cache_file, "w") as f:
441
- json.dump(cache_data, f, indent=2)
564
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
565
+ # Write a new file and move it into place rather than writing
566
+ # through whatever already sits at the destination. A mode passed to
567
+ # `os.open` only applies when it creates the file, so truncating an
568
+ # existing group/world-writable cache would keep that mode and
569
+ # publish what was just written; and `os.replace` is atomic, so a
570
+ # reader never observes a half-written cache when serialization
571
+ # fails part way. `O_EXCL` requires the temporary name to be new,
572
+ # and 0600 is applied at creation so the umask cannot widen it.
573
+ tmp_path = self._cache_file.with_name(
574
+ f"{self._cache_file.name}.{os.getpid()}.tmp"
575
+ )
576
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
577
+ fd = os.open(tmp_path, flags, 0o600)
578
+ try:
579
+ with os.fdopen(fd, "w") as f:
580
+ json.dump(cache_data, f, indent=2)
581
+ os.replace(tmp_path, self._cache_file)
582
+ except BaseException:
583
+ try:
584
+ os.unlink(tmp_path)
585
+ except OSError:
586
+ pass
587
+ raise
442
588
 
443
589
  except Exception as e:
444
590
  logger.warning(f"Failed to save discovery cache: {e}")
@@ -575,8 +721,12 @@ def get_discovery() -> EnvironmentDiscovery:
575
721
 
576
722
 
577
723
  def reset_discovery() -> None:
578
- """Reset the global discovery instance (useful for testing)."""
724
+ """Drop the global discovery singleton (useful for testing).
725
+
726
+ Does **not** delete the on-disk cache. The cache is now a persistent
727
+ per-user file under `$XDG_CACHE_HOME`; wiping it belongs on an explicit
728
+ [`EnvironmentDiscovery.clear_cache`][] call so test fixtures that only
729
+ need a fresh singleton cannot clobber a developer's real cache.
730
+ """
579
731
  global _global_discovery
580
- if _global_discovery is not None:
581
- _global_discovery.clear_cache()
582
732
  _global_discovery = None
@@ -12,8 +12,10 @@ import sys
12
12
  import typer
13
13
  from openenv.cli.commands import (
14
14
  build,
15
+ catalog,
15
16
  collect,
16
17
  fork,
18
+ harbor,
17
19
  import_env,
18
20
  init,
19
21
  push,
@@ -37,9 +39,14 @@ app.command(name="import", help="Import a third-party environment into OpenEnv")
37
39
  app.command(name="build", help="Build Docker images for OpenEnv environments")(
38
40
  build.build
39
41
  )
40
- app.command(
41
- name="validate", help="Validate environment structure and deployment readiness"
42
- )(validate.validate)
42
+ app.add_typer(
43
+ harbor.app,
44
+ name="harbor",
45
+ help="Run Harbor tasks with token-level capture (requires: pip install openenv[harbor])",
46
+ )
47
+ app.command(name="validate", help="Validate a package against the OpenEnv quality bar")(
48
+ validate.validate
49
+ )
43
50
  app.command(
44
51
  name="push",
45
52
  help="Push an OpenEnv environment to Hugging Face Spaces or custom registry",
@@ -60,6 +67,8 @@ app.command(
60
67
  name="collect",
61
68
  help="Collect rollouts from a deployed OpenEnv environment",
62
69
  )(collect.collect)
70
+ app.add_typer(catalog.app, name="catalog")
71
+ app.command(name="discover")(catalog.discover)
63
72
 
64
73
 
65
74
  # Entry point for setuptools
@@ -0,0 +1,121 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+
3
+ """Metadata catalog production and inspection. No environment is instantiated."""
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from openenv.discovery import (
10
+ build_catalog,
11
+ CatalogError,
12
+ load_catalog,
13
+ resolve_entry,
14
+ search_catalog,
15
+ write_catalog,
16
+ )
17
+
18
+ app = typer.Typer(
19
+ help="Build and inspect versioned, metadata-only environment catalogs."
20
+ )
21
+
22
+
23
+ @app.command("build")
24
+ def build(
25
+ repository: Path = typer.Option(..., help="Local Git repository to read."),
26
+ repository_uri: str = typer.Option(..., help="Declared GitHub HTTPS clone URI."),
27
+ publisher: str = typer.Option(..., help="Explicit catalog publication authority."),
28
+ output: Path = typer.Option(..., help="Destination snapshot JSON."),
29
+ revision: str = typer.Option("HEAD", help="Source commit or ref."),
30
+ root: str = typer.Option("envs", help="Direct-child environment inventory root."),
31
+ namespace: str = typer.Option("openenv", help="Publisher-owned namespace."),
32
+ ) -> None:
33
+ """Build from committed metadata; uncommitted files and candidate code are not used."""
34
+ try:
35
+ snapshot = build_catalog(
36
+ repository,
37
+ repository_uri=repository_uri,
38
+ publisher=publisher,
39
+ namespace=namespace,
40
+ revision=revision,
41
+ root=root,
42
+ )
43
+ write_catalog(snapshot, output)
44
+ except CatalogError as error:
45
+ typer.echo(f"Catalog error: {error}", err=True)
46
+ raise typer.Exit(1) from error
47
+ typer.echo(
48
+ f"{len(snapshot.entries)} records, snapshot {snapshot.digest}, "
49
+ f"complete={str(snapshot.complete).lower()}",
50
+ err=True,
51
+ )
52
+ for issue in snapshot.issues:
53
+ typer.echo(f"{issue.severity}: {issue.path}: {issue.message}", err=True)
54
+ if not snapshot.complete:
55
+ raise typer.Exit(1)
56
+
57
+
58
+ @app.command("inspect")
59
+ def inspect(
60
+ identifier: str = typer.Argument(..., help="Exact revision-card identifier."),
61
+ catalog: Path = typer.Option(..., help="Complete configured snapshot."),
62
+ ) -> None:
63
+ """Return the complete selected entry without guessing or fetching a URL."""
64
+ try:
65
+ entry = resolve_entry(load_catalog(catalog), identifier)
66
+ except CatalogError as error:
67
+ typer.echo(f"Catalog error: {error}", err=True)
68
+ raise typer.Exit(1) from error
69
+ typer.echo(entry.model_dump_json(by_alias=True, exclude_none=True, indent=2))
70
+
71
+
72
+ def discover(
73
+ query: str = typer.Argument("", help="Task to find an environment for."),
74
+ catalog: Path = typer.Option(
75
+ ..., help="Complete, explicitly configured local snapshot."
76
+ ),
77
+ limit: int = typer.Option(20, min=1, max=1000),
78
+ filter: list[str] = typer.Option([], "--filter", help="Exact field=value filter."),
79
+ as_json: bool = typer.Option(False, "--json", help="Emit complete ARD entry data."),
80
+ ) -> None:
81
+ """Find unfamiliar environments by task before any installation or execution."""
82
+ filters = {}
83
+ for item in filter:
84
+ key, separator, value = item.partition("=")
85
+ if not separator or not key or key in filters:
86
+ typer.echo(
87
+ "Catalog error: filters must be distinct field=value pairs", err=True
88
+ )
89
+ raise typer.Exit(1)
90
+ filters[key] = value
91
+ try:
92
+ snapshot = load_catalog(catalog)
93
+ matches = search_catalog(snapshot, query, filters=filters, limit=limit)
94
+ except CatalogError as error:
95
+ typer.echo(f"Catalog error: {error}", err=True)
96
+ raise typer.Exit(1) from error
97
+ if as_json:
98
+ results = [
99
+ {
100
+ **match.entry.model_dump(by_alias=True, exclude_none=True, mode="json"),
101
+ "score": match.score,
102
+ }
103
+ for match in matches
104
+ ]
105
+ typer.echo(
106
+ json.dumps(
107
+ {"results": results, "snapshot": snapshot.digest, "metadataOnly": True},
108
+ indent=2,
109
+ ensure_ascii=False,
110
+ )
111
+ )
112
+ return
113
+ for match in matches:
114
+ card = match.entry.data
115
+ typer.echo(f"{match.entry.display_name}: {card.description}")
116
+ typer.echo(f" source: {card.source.uri}")
117
+ typer.echo(f" path: {card.source.path}")
118
+ typer.echo(f" revision: {card.source.revision}")
119
+ typer.echo(f" license: {card.license}; artifact: {card.artifact_availability}")
120
+ typer.echo(f" identifier: {match.entry.identifier}")
121
+ typer.echo("Metadata only. Execution approval remains a separate decision.")
@@ -141,7 +141,7 @@ def _build_llm_model_step(
141
141
  model: str,
142
142
  *,
143
143
  llm_endpoint: str | None,
144
- llm_port: int,
144
+ llm_port: int | None,
145
145
  temperature: float,
146
146
  max_tokens: int,
147
147
  system_prompt: str | None = None,
@@ -152,15 +152,19 @@ def _build_llm_model_step(
152
152
  # Self-hosted OpenAI-compatible endpoint (vLLM, TGI, Ollama, ...).
153
153
  from openenv.core.llm_client import OpenAIClient
154
154
 
155
- client: LLMClient = OpenAIClient(
156
- endpoint=llm_endpoint,
157
- port=llm_port,
158
- model=model,
159
- api_key=os.getenv("OPENAI_API_KEY") or "not-needed",
160
- system_prompt=effective_system_prompt,
161
- temperature=temperature,
162
- max_tokens=max_tokens,
163
- )
155
+ try:
156
+ client: LLMClient = OpenAIClient(
157
+ endpoint=llm_endpoint,
158
+ port=llm_port,
159
+ model=model,
160
+ api_key=os.getenv("OPENAI_API_KEY") or "not-needed",
161
+ system_prompt=effective_system_prompt,
162
+ temperature=temperature,
163
+ max_tokens=max_tokens,
164
+ )
165
+ except ValueError as exc:
166
+ raise typer.BadParameter(str(exc), param_hint="--llm-endpoint") from exc
167
+ console.print(f"[cyan]LLM endpoint:[/cyan] {client.base_url}")
164
168
  else:
165
169
  client = create_llm_client(
166
170
  provider=provider,
@@ -280,13 +284,23 @@ def collect(
280
284
  str | None,
281
285
  typer.Option(
282
286
  "--llm-endpoint",
283
- help="OpenAI-compatible endpoint URL (for self-hosted vLLM/TGI/Ollama).",
287
+ help=(
288
+ "Base URL of a self-hosted OpenAI-compatible server "
289
+ "(vLLM/TGI/Ollama), e.g. http://localhost:8000. /v1 is appended "
290
+ "when the URL has no path; a URL with a path is used as-is."
291
+ ),
284
292
  ),
285
293
  ] = None,
286
294
  llm_port: Annotated[
287
- int,
288
- typer.Option("--llm-port", help="Port for self-hosted LLM endpoint."),
289
- ] = 8000,
295
+ int | None,
296
+ typer.Option(
297
+ "--llm-port",
298
+ help=(
299
+ "Port appended to --llm-endpoint when the URL does not include one. "
300
+ "No default: earlier releases assumed 8000."
301
+ ),
302
+ ),
303
+ ] = None,
290
304
  temperature: Annotated[
291
305
  float, typer.Option("--temperature", help="Sampling temperature.")
292
306
  ] = 0.2,
@@ -364,6 +378,20 @@ def collect(
364
378
  factory = _build_session_factory(
365
379
  env, base_url, dataset_config=parsed_dataset_config
366
380
  )
381
+
382
+ if uses_llm_teacher:
383
+ model_step = _build_llm_model_step(
384
+ provider=provider,
385
+ model=model, # type: ignore[arg-type] # validated above
386
+ llm_endpoint=llm_endpoint,
387
+ llm_port=llm_port,
388
+ temperature=temperature,
389
+ max_tokens=max_tokens,
390
+ system_prompt=system_prompt,
391
+ )
392
+ else:
393
+ model_step = _build_scripted_model_step()
394
+
367
395
  serializer = RolloutSerializer(output_dir)
368
396
  serializer.write_metadata(
369
397
  {
@@ -379,19 +407,6 @@ def collect(
379
407
  }
380
408
  )
381
409
 
382
- if uses_llm_teacher:
383
- model_step = _build_llm_model_step(
384
- provider=provider,
385
- model=model, # type: ignore[arg-type] # validated above
386
- llm_endpoint=llm_endpoint,
387
- llm_port=llm_port,
388
- temperature=temperature,
389
- max_tokens=max_tokens,
390
- system_prompt=system_prompt,
391
- )
392
- else:
393
- model_step = _build_scripted_model_step()
394
-
395
410
  should_keep = None if keep_losses else (lambda record: record.reward >= 0.0)
396
411
 
397
412
  collect_runner = CollectRunner(