benchmax 0.2.0__tar.gz → 0.2.2.dev0__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 (64) hide show
  1. benchmax-0.2.2.dev0/PKG-INFO +109 -0
  2. benchmax-0.2.2.dev0/README.md +94 -0
  3. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/pyproject.toml +2 -2
  4. benchmax-0.2.2.dev0/src/benchmax/auth.py +187 -0
  5. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/bundle.py +29 -55
  6. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/README.md +2 -3
  7. benchmax-0.2.2.dev0/src/benchmax/envs/base/README.md +80 -0
  8. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/base/__init__.py +1 -1
  9. benchmax-0.2.2.dev0/src/benchmax/envs/base/dataset.py +80 -0
  10. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/base/env.py +45 -55
  11. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/dataset.py +11 -1
  12. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/environment.py +63 -78
  13. benchmax-0.2.2.dev0/src/benchmax/envs/harbor/README.md +86 -0
  14. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/bundled_agent.py +9 -27
  15. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/credentials.py +3 -8
  16. benchmax-0.2.2.dev0/src/benchmax/envs/harbor/dataset.py +303 -0
  17. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/env.py +229 -84
  18. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/types.py +1 -3
  19. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/identity.py +2 -6
  20. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/logging.py +1 -1
  21. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/shared_types.py +24 -7
  22. benchmax-0.2.2.dev0/src/benchmax/rewards/README.md +75 -0
  23. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/__init__.py +9 -9
  24. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/adaptive.py +3 -9
  25. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/deterministic.py +3 -3
  26. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/diversity.py +5 -4
  27. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/judge.py +10 -34
  28. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/prompts.py +5 -13
  29. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/rubric.py +15 -37
  30. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/scoring.py +11 -35
  31. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/conftest.py +1 -0
  32. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/bundle/test_artifact.py +3 -12
  33. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/bundle/test_source_capture.py +7 -26
  34. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_base_dataset.py +33 -2
  35. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_base_env_group.py +209 -64
  36. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_contract_types.py +7 -5
  37. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_environment_group.py +109 -44
  38. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_example_id.py +0 -1
  39. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/fakes/model_server.py +1 -3
  40. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/harbor/test_bundled_agent.py +12 -24
  41. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/harbor/test_harbor_dataset.py +125 -6
  42. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/harbor/test_harbor_env.py +200 -97
  43. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_adaptive.py +1 -4
  44. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_deterministic.py +7 -5
  45. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_diversity.py +0 -1
  46. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_diversity_env.py +9 -22
  47. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_judge.py +8 -4
  48. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_rubric.py +1 -4
  49. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_rubric_rewards.py +1 -4
  50. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/test_auth.py +27 -2
  51. benchmax-0.2.0/PKG-INFO +0 -193
  52. benchmax-0.2.0/README.md +0 -178
  53. benchmax-0.2.0/src/benchmax/auth.py +0 -109
  54. benchmax-0.2.0/src/benchmax/envs/base/README.md +0 -74
  55. benchmax-0.2.0/src/benchmax/envs/base/dataset.py +0 -75
  56. benchmax-0.2.0/src/benchmax/envs/harbor/README.md +0 -156
  57. benchmax-0.2.0/src/benchmax/envs/harbor/dataset.py +0 -158
  58. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/.gitignore +0 -0
  59. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/pytest.ini +0 -0
  60. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/__init__.py +2 -2
  61. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/base/openai_types.py +0 -0
  62. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/__init__.py +0 -0
  63. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/dep_check.py +0 -0
  64. {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/conftest.py +2 -2
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: benchmax
3
+ Version: 0.2.2.dev0
4
+ Summary: Platform-independent runtime for grouped LLM environments
5
+ Author: benchmax Authors
6
+ Classifier: Operating System :: OS Independent
7
+ Classifier: Programming Language :: Python :: 3
8
+ Requires-Python: ==3.12.*
9
+ Requires-Dist: cloudpickle>=3.0.0
10
+ Requires-Dist: openai>=2.15.0
11
+ Requires-Dist: packaging>=24.0
12
+ Provides-Extra: harbor
13
+ Requires-Dist: harbor<0.19,>=0.18.0; extra == 'harbor'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # benchmax
17
+
18
+ benchmax envs is where you define datasets, how to execute the rollout, and scoring each rollout.
19
+
20
+ for installation and project setup, start with the [main readme](../../README.md#get-started). working environments live in [`examples/`](../../examples/).
21
+
22
+ ## choose an environment
23
+
24
+ all benchmax environments implement the same dataset and rollout contracts. choose the adapter based on who owns the agent loop:
25
+
26
+ | environment | use it when | what it provides |
27
+ | --- | --- | --- |
28
+ | [`BaseEnv`](src/benchmax/envs/base/README.md) | default environment to extend - runs a simple loop with the option to make tool calls | chat completions, tool dispatch, turn limits, and reward hooks |
29
+ | [`HarborEnv`](src/benchmax/envs/harbor/README.md) | you already have a Harbor task or harness | Harbor agents, sandboxes, verifiers, and RewardKit integration |
30
+ | [`Environment`](src/benchmax/envs/README.md) | extend `Environment` if you need custom behavior not covered by `BaseEnv` and `HarborEnv` | the fundamental dataset, group execution, and outcome contracts |
31
+
32
+ most custom environments should extend `BaseEnv`. most Harbor users configure `HarborEnv` directly rather than subclassing it.
33
+
34
+ ## architecture
35
+
36
+ an environment defines its dataset and how a group of rollouts runs against each example.
37
+
38
+ ```text
39
+ Environment
40
+ ├── create_dataset(split, base_dir, max_examples)
41
+ │ └── Dataset
42
+ │ └── Example(id, payload)
43
+ └── run_group(requests)
44
+ ├── run_rollout(request) × group_size → RolloutAttempt × group_size
45
+ ├── adapter-specific scoring
46
+ ├── optional group-relative scoring
47
+ └── RolloutOutcome(rewards, termination_reason, error)
48
+ ```
49
+
50
+ every environment follows this shape. the environment decides what an example contains, how each attempt runs, which tools are available, and how the result is scored.
51
+
52
+ ## datasets
53
+
54
+ `create_dataset` receives a `train` or `eval` split and returns a fixed, ordered `Dataset` of `Example` objects.
55
+
56
+ each example contains:
57
+
58
+ - a stable id used to identify the datapoint across runs;
59
+ - an environment-owned payload consumed by its rollout implementation.
60
+
61
+ the optional `max_examples` argument limits how many examples are returned. when the data source supports it, the environment should stop loading once it reaches that limit.
62
+
63
+ `JsonlDataset`, Harbor datasets, and custom datasets all produce the same fundamental `Dataset` type. the trainer and validation flow do not depend on the source file format.
64
+
65
+ ## tools
66
+
67
+ `BaseEnv` exposes OpenAI-compatible function tools through `list_tools` and executes them through `run_tool`. an environment can provide no tools, one tool, or a collection of stateful tools.
68
+
69
+ with `HarborEnv`, the Harbor agent and harness define the available tools and how they interact with the sandbox. benchmax does not convert Harbor tools into `BaseEnv` tools.
70
+
71
+ ## execution and scoring
72
+
73
+ `run_group` receives multiple rollout requests for the same example, runs them concurrently, waits for all siblings, and returns one `RolloutOutcome` for each request.
74
+
75
+ successful scoring hooks return their named reward components. operational failures return no rewards and do not cancel successful siblings; the trainer treats absent components as zero. partial attempts that reach a context, output, turn, or tool limit can still be scored.
76
+
77
+ - `BaseEnv` runs the model and tool loop, then passes the transcript and example payload to `compute_reward`. `compute_group_rewards` can score the completed sibling group.
78
+ - `HarborEnv` runs the configured Harbor agent and sandbox, then preserves its verifier or RewardKit reward components.
79
+
80
+ ### helpers
81
+
82
+ `benchmax.rewards` provides deterministic text helpers, model judges, rubrics, ranking, adaptive rubrics, and diversity scoring for `BaseEnv` and direct `Environment` implementations. see the [rewards guide](src/benchmax/rewards/README.md).
83
+
84
+ Harbor environments normally use their harness verifier and RewardKit instead of benchmax reward helpers.
85
+
86
+ ## bundling
87
+
88
+ a bundle contains the environment class, its constructor arguments, the project-local source it needs, and its declared remote dependencies.
89
+
90
+ ```text
91
+ environment class + constructor arguments
92
+ + local source
93
+ + dependency metadata
94
+
95
+
96
+ portable bundle
97
+ ```
98
+
99
+ benchmax creates the portable artifact so the same environment can be loaded outside the author's checkout. `castform` handles uploading the bundle, validating it remotely, and using it for training.
100
+
101
+ ## further reading
102
+
103
+ - [base environment guide](src/benchmax/envs/base/README.md)
104
+ - [harbor environment guide](src/benchmax/envs/harbor/README.md)
105
+ - [reward helpers](src/benchmax/rewards/README.md)
106
+ - [examples](../../examples/)
107
+ - [development instructions](../../README.md#development)
108
+
109
+ apache 2.0 © 2026 CGFT Inc.
@@ -0,0 +1,94 @@
1
+ # benchmax
2
+
3
+ benchmax envs is where you define datasets, how to execute the rollout, and scoring each rollout.
4
+
5
+ for installation and project setup, start with the [main readme](../../README.md#get-started). working environments live in [`examples/`](../../examples/).
6
+
7
+ ## choose an environment
8
+
9
+ all benchmax environments implement the same dataset and rollout contracts. choose the adapter based on who owns the agent loop:
10
+
11
+ | environment | use it when | what it provides |
12
+ | --- | --- | --- |
13
+ | [`BaseEnv`](src/benchmax/envs/base/README.md) | default environment to extend - runs a simple loop with the option to make tool calls | chat completions, tool dispatch, turn limits, and reward hooks |
14
+ | [`HarborEnv`](src/benchmax/envs/harbor/README.md) | you already have a Harbor task or harness | Harbor agents, sandboxes, verifiers, and RewardKit integration |
15
+ | [`Environment`](src/benchmax/envs/README.md) | extend `Environment` if you need custom behavior not covered by `BaseEnv` and `HarborEnv` | the fundamental dataset, group execution, and outcome contracts |
16
+
17
+ most custom environments should extend `BaseEnv`. most Harbor users configure `HarborEnv` directly rather than subclassing it.
18
+
19
+ ## architecture
20
+
21
+ an environment defines its dataset and how a group of rollouts runs against each example.
22
+
23
+ ```text
24
+ Environment
25
+ ├── create_dataset(split, base_dir, max_examples)
26
+ │ └── Dataset
27
+ │ └── Example(id, payload)
28
+ └── run_group(requests)
29
+ ├── run_rollout(request) × group_size → RolloutAttempt × group_size
30
+ ├── adapter-specific scoring
31
+ ├── optional group-relative scoring
32
+ └── RolloutOutcome(rewards, termination_reason, error)
33
+ ```
34
+
35
+ every environment follows this shape. the environment decides what an example contains, how each attempt runs, which tools are available, and how the result is scored.
36
+
37
+ ## datasets
38
+
39
+ `create_dataset` receives a `train` or `eval` split and returns a fixed, ordered `Dataset` of `Example` objects.
40
+
41
+ each example contains:
42
+
43
+ - a stable id used to identify the datapoint across runs;
44
+ - an environment-owned payload consumed by its rollout implementation.
45
+
46
+ the optional `max_examples` argument limits how many examples are returned. when the data source supports it, the environment should stop loading once it reaches that limit.
47
+
48
+ `JsonlDataset`, Harbor datasets, and custom datasets all produce the same fundamental `Dataset` type. the trainer and validation flow do not depend on the source file format.
49
+
50
+ ## tools
51
+
52
+ `BaseEnv` exposes OpenAI-compatible function tools through `list_tools` and executes them through `run_tool`. an environment can provide no tools, one tool, or a collection of stateful tools.
53
+
54
+ with `HarborEnv`, the Harbor agent and harness define the available tools and how they interact with the sandbox. benchmax does not convert Harbor tools into `BaseEnv` tools.
55
+
56
+ ## execution and scoring
57
+
58
+ `run_group` receives multiple rollout requests for the same example, runs them concurrently, waits for all siblings, and returns one `RolloutOutcome` for each request.
59
+
60
+ successful scoring hooks return their named reward components. operational failures return no rewards and do not cancel successful siblings; the trainer treats absent components as zero. partial attempts that reach a context, output, turn, or tool limit can still be scored.
61
+
62
+ - `BaseEnv` runs the model and tool loop, then passes the transcript and example payload to `compute_reward`. `compute_group_rewards` can score the completed sibling group.
63
+ - `HarborEnv` runs the configured Harbor agent and sandbox, then preserves its verifier or RewardKit reward components.
64
+
65
+ ### helpers
66
+
67
+ `benchmax.rewards` provides deterministic text helpers, model judges, rubrics, ranking, adaptive rubrics, and diversity scoring for `BaseEnv` and direct `Environment` implementations. see the [rewards guide](src/benchmax/rewards/README.md).
68
+
69
+ Harbor environments normally use their harness verifier and RewardKit instead of benchmax reward helpers.
70
+
71
+ ## bundling
72
+
73
+ a bundle contains the environment class, its constructor arguments, the project-local source it needs, and its declared remote dependencies.
74
+
75
+ ```text
76
+ environment class + constructor arguments
77
+ + local source
78
+ + dependency metadata
79
+
80
+
81
+ portable bundle
82
+ ```
83
+
84
+ benchmax creates the portable artifact so the same environment can be loaded outside the author's checkout. `castform` handles uploading the bundle, validating it remotely, and using it for training.
85
+
86
+ ## further reading
87
+
88
+ - [base environment guide](src/benchmax/envs/base/README.md)
89
+ - [harbor environment guide](src/benchmax/envs/harbor/README.md)
90
+ - [reward helpers](src/benchmax/rewards/README.md)
91
+ - [examples](../../examples/)
92
+ - [development instructions](../../README.md#development)
93
+
94
+ apache 2.0 © 2026 CGFT Inc.
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "benchmax"
3
- version = "0.2.0"
3
+ version = "0.2.2.dev0"
4
4
  description = "Platform-independent runtime for grouped LLM environments"
5
5
  readme = "README.md"
6
- authors = [{ name = "BenchMax Authors" }]
6
+ authors = [{ name = "benchmax Authors" }]
7
7
  requires-python = "==3.12.*"
8
8
  dependencies = [
9
9
  "cloudpickle>=3.0.0",
@@ -0,0 +1,187 @@
1
+ """Explicit, call-time authentication for model requests.
2
+
3
+ benchmax defines only the runtime contract. Platform packages and execution
4
+ runtimes provide concrete credential sources and bind injected credentials.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextvars
11
+ import threading
12
+ from collections.abc import AsyncIterator, Iterator, Mapping
13
+ from contextlib import contextmanager
14
+ from contextvars import ContextVar
15
+ from dataclasses import dataclass, field
16
+ from typing import Protocol, runtime_checkable
17
+
18
+ import httpx
19
+
20
+ __all__ = [
21
+ "InjectedAuth",
22
+ "ModelAuth",
23
+ "ModelRequestContext",
24
+ "RequestModelAuth",
25
+ "StaticBearerAuth",
26
+ "bind_model_auth",
27
+ ]
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ModelRequestContext:
32
+ """Identity of the model request about to be authorized."""
33
+
34
+ base_url: str
35
+ model: str
36
+ rollout_id: str
37
+
38
+
39
+ @runtime_checkable
40
+ class ModelAuth(Protocol):
41
+ """Return headers immediately before each model HTTP request."""
42
+
43
+ async def headers_for_request(
44
+ self,
45
+ context: ModelRequestContext,
46
+ ) -> Mapping[str, str]: ...
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class StaticBearerAuth:
51
+ """Explicit bearer authentication for providers with a stable API key."""
52
+
53
+ token: str = field(repr=False)
54
+
55
+ def __post_init__(self) -> None:
56
+ if not isinstance(self.token, str) or not self.token:
57
+ raise ValueError("bearer token must be a non-empty string")
58
+
59
+ async def headers_for_request(
60
+ self,
61
+ context: ModelRequestContext,
62
+ ) -> Mapping[str, str]:
63
+ del context
64
+ return {"Authorization": f"Bearer {self.token}"}
65
+
66
+
67
+ class RequestModelAuth(httpx.Auth):
68
+ """Apply a :class:`ModelAuth` immediately before each HTTP request.
69
+
70
+ Both sync and async OpenAI-compatible clients use this adapter, so model
71
+ credential selection never falls back to an SDK environment variable or a
72
+ separate token resolver. When a sync client runs inside an active event
73
+ loop, auth is resolved in a context-preserving helper thread; custom
74
+ providers used there must not depend on primitives bound to another loop.
75
+ """
76
+
77
+ def __init__(self, auth: ModelAuth, context: ModelRequestContext) -> None:
78
+ if not isinstance(auth, ModelAuth):
79
+ raise TypeError("request auth must implement ModelAuth")
80
+ self._auth = auth
81
+ self._context = context
82
+
83
+ def sync_auth_flow(
84
+ self,
85
+ request: httpx.Request,
86
+ ) -> Iterator[httpx.Request]:
87
+ headers = _resolve_headers_sync(self._auth, self._context)
88
+ for name, value in headers.items():
89
+ request.headers[name] = value
90
+ yield request
91
+
92
+ async def async_auth_flow(
93
+ self,
94
+ request: httpx.Request,
95
+ ) -> AsyncIterator[httpx.Request]:
96
+ headers = await self._auth.headers_for_request(self._context)
97
+ for name, value in headers.items():
98
+ request.headers[name] = value
99
+ yield request
100
+
101
+
102
+ def _resolve_headers_sync(
103
+ auth: ModelAuth,
104
+ context: ModelRequestContext,
105
+ ) -> Mapping[str, str]:
106
+ """Resolve async ``ModelAuth`` from a synchronous HTTP client.
107
+
108
+ RAG search backends expose synchronous embedding callables. When one is
109
+ invoked from an async environment tool, its event loop is already running;
110
+ resolve the auth coroutine in a context-preserving helper thread rather
111
+ than attempting a nested event loop.
112
+ """
113
+
114
+ async def resolve() -> Mapping[str, str]:
115
+ return await auth.headers_for_request(context)
116
+
117
+ try:
118
+ asyncio.get_running_loop()
119
+ except RuntimeError:
120
+ return asyncio.run(resolve())
121
+
122
+ copied_context = contextvars.copy_context()
123
+ result: list[Mapping[str, str]] = []
124
+ failure: list[BaseException] = []
125
+
126
+ def run() -> None:
127
+ try:
128
+ result.append(copied_context.run(lambda: asyncio.run(resolve())))
129
+ except BaseException as error: # propagate the original auth failure
130
+ failure.append(error)
131
+
132
+ thread = threading.Thread(target=run, daemon=True)
133
+ thread.start()
134
+ thread.join()
135
+ if failure:
136
+ raise failure[0]
137
+ if not result:
138
+ raise RuntimeError("model authentication did not return headers")
139
+ return result[0]
140
+
141
+
142
+ _BOUND_MODEL_AUTH: ContextVar[Mapping[str, ModelAuth] | None] = ContextVar(
143
+ "benchmax_bound_model_auth",
144
+ default=None,
145
+ )
146
+
147
+
148
+ @dataclass(frozen=True, slots=True)
149
+ class InjectedAuth:
150
+ """Serializable reference to authentication supplied by the runtime."""
151
+
152
+ name: str
153
+
154
+ def __post_init__(self) -> None:
155
+ if not isinstance(self.name, str) or not self.name.strip():
156
+ raise ValueError("injected auth name must be a non-empty string")
157
+
158
+ async def headers_for_request(
159
+ self,
160
+ context: ModelRequestContext,
161
+ ) -> Mapping[str, str]:
162
+ providers = _BOUND_MODEL_AUTH.get()
163
+ provider = providers.get(self.name) if providers is not None else None
164
+ if provider is None:
165
+ raise RuntimeError(f"No runtime model-auth provider was injected for {self.name!r}.")
166
+ if isinstance(provider, InjectedAuth):
167
+ raise RuntimeError(
168
+ f"Injected model-auth provider {self.name!r} cannot reference another InjectedAuth."
169
+ )
170
+ return await provider.headers_for_request(context)
171
+
172
+
173
+ @contextmanager
174
+ def bind_model_auth(providers: Mapping[str, ModelAuth]) -> Iterator[None]:
175
+ """Bind runtime providers for the current async execution context."""
176
+
177
+ normalized = dict(providers)
178
+ for name, provider in normalized.items():
179
+ if not isinstance(name, str) or not name.strip():
180
+ raise ValueError("model-auth provider names must be non-empty strings")
181
+ if not isinstance(provider, ModelAuth):
182
+ raise TypeError(f"model-auth provider {name!r} does not implement ModelAuth")
183
+ token = _BOUND_MODEL_AUTH.set(normalized)
184
+ try:
185
+ yield
186
+ finally:
187
+ _BOUND_MODEL_AUTH.reset(token)
@@ -8,7 +8,6 @@ import io
8
8
  import json
9
9
  import logging
10
10
  import pickle
11
- import re
12
11
  import site
13
12
  import sys
14
13
  import threading
@@ -20,11 +19,10 @@ from types import CodeType, ModuleType
20
19
  from typing import Any
21
20
 
22
21
  import cloudpickle
23
- from packaging.requirements import InvalidRequirement, Requirement
24
- from packaging.utils import canonicalize_name
25
-
26
22
  from benchmax.envs.environment import Environment
27
23
  from benchmax.envs.shared_types import RolloutAttempt
24
+ from packaging.requirements import InvalidRequirement, Requirement
25
+ from packaging.utils import canonicalize_name
28
26
 
29
27
  logger = logging.getLogger(__name__)
30
28
 
@@ -34,7 +32,7 @@ class BundlingError(Exception):
34
32
 
35
33
 
36
34
  class IncompatibleRuntimeError(Exception):
37
- """Bundle metadata is incompatible with the current BenchMax runtime."""
35
+ """Bundle metadata is incompatible with the current benchmax runtime."""
38
36
 
39
37
 
40
38
  class IncompatiblePythonError(IncompatibleRuntimeError):
@@ -42,7 +40,7 @@ class IncompatiblePythonError(IncompatibleRuntimeError):
42
40
 
43
41
 
44
42
  class IncompatibleBenchmaxError(IncompatibleRuntimeError):
45
- """Loader's BenchMax major.minor doesn't match the bundle's benchmax_version."""
43
+ """Loader's benchmax major.minor doesn't match the bundle's benchmax_version."""
46
44
 
47
45
 
48
46
  # register_pickle_by_value mutates process-global state; serialize against races.
@@ -72,9 +70,7 @@ class BundleMetadata:
72
70
  value = getattr(self, name)
73
71
  if not isinstance(value, str) or not value.strip():
74
72
  raise ValueError(f"{name} must be a non-empty string")
75
- if self.env_class_source is not None and not isinstance(
76
- self.env_class_source, str
77
- ):
73
+ if self.env_class_source is not None and not isinstance(self.env_class_source, str):
78
74
  raise TypeError("env_class_source must be a string or None")
79
75
 
80
76
  def to_json_bytes(self) -> bytes:
@@ -93,7 +89,7 @@ class BundleMetadata:
93
89
  ).encode("utf-8")
94
90
 
95
91
  @classmethod
96
- def from_json_bytes(cls, data: bytes) -> "BundleMetadata":
92
+ def from_json_bytes(cls, data: bytes) -> BundleMetadata:
97
93
  try:
98
94
  d = json.loads(data.decode("utf-8"))
99
95
  except (AttributeError, UnicodeDecodeError, json.JSONDecodeError) as exc:
@@ -104,7 +100,7 @@ class BundleMetadata:
104
100
  if unknown:
105
101
  raise ValueError(
106
102
  f"bundle metadata has unsupported keys {unknown}; "
107
- "re-bundle with a current BenchMax release"
103
+ "re-bundle with a current benchmax release"
108
104
  )
109
105
  try:
110
106
  pip_dependencies = d["pip_dependencies"]
@@ -155,7 +151,7 @@ def bundle_digest(bundle: Bundle) -> str:
155
151
 
156
152
 
157
153
  def validate_bundle_compatibility(metadata: BundleMetadata) -> None:
158
- """Reject metadata built for a different Python or BenchMax runtime.
154
+ """Reject metadata built for a different Python or benchmax runtime.
159
155
 
160
156
  This check never reads the pickle or installs environment dependencies, so
161
157
  execution runtimes can call it before performing either higher-risk step.
@@ -174,20 +170,20 @@ def validate_bundle_compatibility(metadata: BundleMetadata) -> None:
174
170
  current_benchmax = _benchmax_version()
175
171
  if metadata.benchmax_version == "unknown" or current_benchmax == "unknown":
176
172
  raise IncompatibleBenchmaxError(
177
- "Cannot verify BenchMax compatibility because the bundle or "
178
- "runtime version is unknown. Install BenchMax as a versioned package."
173
+ "Cannot verify benchmax compatibility because the bundle or "
174
+ "runtime version is unknown. Install benchmax as a versioned package."
179
175
  )
180
176
  bundle_series = _version_major_minor(metadata.benchmax_version)
181
177
  current_series = _version_major_minor(current_benchmax)
182
178
  if bundle_series is None or current_series is None:
183
179
  raise IncompatibleBenchmaxError(
184
- f"Cannot parse BenchMax versions (bundle {metadata.benchmax_version}, "
180
+ f"Cannot parse benchmax versions (bundle {metadata.benchmax_version}, "
185
181
  f"runtime {current_benchmax}); expected major.minor[.patch]."
186
182
  )
187
183
  if bundle_series != current_series:
188
184
  raise IncompatibleBenchmaxError(
189
- f"Bundle was packaged with BenchMax {metadata.benchmax_version} "
190
- f"but this runtime uses BenchMax {current_benchmax}; "
185
+ f"Bundle was packaged with benchmax {metadata.benchmax_version} "
186
+ f"but this runtime uses benchmax {current_benchmax}; "
191
187
  "major.minor versions must match."
192
188
  )
193
189
 
@@ -238,7 +234,7 @@ def dump_bundle(
238
234
  benchmax_version = _benchmax_version()
239
235
  if benchmax_version == "unknown":
240
236
  raise BundlingError(
241
- "Cannot determine the BenchMax package version; install BenchMax as "
237
+ "Cannot determine the benchmax package version; install benchmax as "
242
238
  "a versioned package before creating a bundle"
243
239
  )
244
240
  local_modules = local_modules or []
@@ -249,17 +245,14 @@ def dump_bundle(
249
245
  for mod in local_modules:
250
246
  if not isinstance(mod, ModuleType):
251
247
  raise BundlingError(
252
- f"local_modules must contain module objects, got "
253
- f"{type(mod).__name__}: {mod!r}"
248
+ f"local_modules must contain module objects, got {type(mod).__name__}: {mod!r}"
254
249
  )
255
250
  cloudpickle.register_pickle_by_value(mod)
256
251
  try:
257
252
  try:
258
253
  pickled = cloudpickle.dumps((env_class, constructor_args))
259
254
  except Exception as e:
260
- raise BundlingError(
261
- f"Failed to serialize {env_class.__name__}: {e}"
262
- ) from e
255
+ raise BundlingError(f"Failed to serialize {env_class.__name__}: {e}") from e
263
256
  finally:
264
257
  for mod in local_modules:
265
258
  try:
@@ -283,9 +276,7 @@ def dump_bundle(
283
276
  registered.append(mod)
284
277
  for _ in range(10):
285
278
  pending = [
286
- m
287
- for m in _unregistered_local_refs(pickled, project_roots)
288
- if m not in seen
279
+ m for m in _unregistered_local_refs(pickled, project_roots) if m not in seen
289
280
  ]
290
281
  if not pending:
291
282
  break
@@ -375,8 +366,7 @@ def load_bundle(
375
366
  *,
376
367
  instantiate: bool = True,
377
368
  ) -> (
378
- Environment[Any, RolloutAttempt]
379
- | tuple[type[Environment[Any, RolloutAttempt]], dict[str, Any]]
369
+ Environment[Any, RolloutAttempt] | tuple[type[Environment[Any, RolloutAttempt]], dict[str, Any]]
380
370
  ):
381
371
  """Unpickle and (optionally) instantiate.
382
372
 
@@ -389,7 +379,7 @@ def load_bundle(
389
379
  If False, return ``(env_class, constructor_args)``.
390
380
 
391
381
  Raises:
392
- IncompatibleRuntimeError: bundle's Python or BenchMax version differs.
382
+ IncompatibleRuntimeError: bundle's Python or benchmax version differs.
393
383
  BundlingError: corrupt bytes or a class that does not implement Environment.
394
384
  """
395
385
  validate_bundle_compatibility(bundle.metadata)
@@ -407,8 +397,7 @@ def load_bundle(
407
397
  env_class, constructor_args = payload
408
398
  if not (isinstance(env_class, type) and issubclass(env_class, Environment)):
409
399
  raise BundlingError(
410
- f"Unpickled class is {type(env_class).__name__}, "
411
- "not an Environment implementation."
400
+ f"Unpickled class is {type(env_class).__name__}, not an Environment implementation."
412
401
  )
413
402
  if not isinstance(constructor_args, dict):
414
403
  raise BundlingError(
@@ -470,8 +459,7 @@ def _ensure_safe_python_version() -> None:
470
459
  v = sys.version_info
471
460
  if (v.major, v.minor) == (3, 13):
472
461
  raise BundlingError(
473
- f"Python {v.major}.{v.minor}.{v.micro} is unsupported. "
474
- "Use Python 3.12 or >= 3.14."
462
+ f"Python {v.major}.{v.minor}.{v.micro} is unsupported. Use Python 3.12 or >= 3.14."
475
463
  )
476
464
 
477
465
 
@@ -485,9 +473,7 @@ def unregistered_local_refs(pickled: bytes) -> list[str]:
485
473
 
486
474
  refs = _referenced_modules(pickled)
487
475
  project_roots = tuple(
488
- root
489
- for name in refs
490
- if (root := _project_root_for_module_name(name)) is not None
476
+ root for name in refs if (root := _project_root_for_module_name(name)) is not None
491
477
  )
492
478
  return _unregistered_local_refs(pickled, project_roots)
493
479
 
@@ -641,9 +627,7 @@ def _imports_from_code(
641
627
  instructions = tuple(dis.get_instructions(code))
642
628
  names.update(_literal_dynamic_imports(instructions))
643
629
  for index, instruction in enumerate(instructions):
644
- if instruction.opname != "IMPORT_NAME" or not isinstance(
645
- instruction.argval, str
646
- ):
630
+ if instruction.opname != "IMPORT_NAME" or not isinstance(instruction.argval, str):
647
631
  continue
648
632
  level = 0
649
633
  fromlist: object = None
@@ -699,14 +683,8 @@ def _literal_dynamic_imports(
699
683
  start -= 1
700
684
  call_setup = instructions[start + 1 : call_index]
701
685
  uses_import_callable = any(
702
- (
703
- item.opname == "LOAD_GLOBAL"
704
- and item.argval in {"__import__", "import_module"}
705
- )
706
- or (
707
- item.opname in {"LOAD_ATTR", "LOAD_METHOD"}
708
- and item.argval == "import_module"
709
- )
686
+ (item.opname == "LOAD_GLOBAL" and item.argval in {"__import__", "import_module"})
687
+ or (item.opname in {"LOAD_ATTR", "LOAD_METHOD"} and item.argval == "import_module")
710
688
  for item in call_setup
711
689
  )
712
690
  if not uses_import_callable:
@@ -715,9 +693,7 @@ def _literal_dynamic_imports(
715
693
  (
716
694
  item.argval
717
695
  for item in call_setup
718
- if item.opname == "LOAD_CONST"
719
- and isinstance(item.argval, str)
720
- and item.argval
696
+ if item.opname == "LOAD_CONST" and isinstance(item.argval, str) and item.argval
721
697
  ),
722
698
  None,
723
699
  )
@@ -812,8 +788,7 @@ def _module_has_declared_distribution(
812
788
  top_level = module_name.partition(".")[0]
813
789
  distributions = importlib_metadata.packages_distributions().get(top_level, ())
814
790
  return any(
815
- canonicalize_name(distribution) in declared_distributions
816
- for distribution in distributions
791
+ canonicalize_name(distribution) in declared_distributions for distribution in distributions
817
792
  )
818
793
 
819
794
 
@@ -862,7 +837,7 @@ def _referenced_modules(pickled: bytes) -> set[str]:
862
837
  def __init__(self, *a: Any, **kw: Any) -> None:
863
838
  pass
864
839
 
865
- def __call__(self, *a: Any, **kw: Any) -> "_Stub":
840
+ def __call__(self, *a: Any, **kw: Any) -> _Stub:
866
841
  return self
867
842
 
868
843
  def __reduce__(self) -> tuple:
@@ -964,8 +939,7 @@ def _module_is_project_local(
964
939
  module = _module_from_spec(module_name, spec)
965
940
 
966
941
  return any(
967
- not _is_site_package_path(path)
968
- and any(path.is_relative_to(root) for root in project_roots)
942
+ not _is_site_package_path(path) and any(path.is_relative_to(root) for root in project_roots)
969
943
  for path in _module_source_paths(module)
970
944
  )
971
945
 
@@ -11,9 +11,8 @@ harbor/ Concrete optional adapter over native Harbor configs
11
11
 
12
12
  Most custom environments inherit [`BaseEnv`](base/env.py). It supplies the
13
13
  conversation loop and optional tool dispatch. Subclasses own dataset semantics,
14
- stable identity, and rewards. Every environment explicitly declares its
15
- complete `reward_keys`; operational failures return that shape with all values
16
- zero and do not cancel or distort siblings.
14
+ stable identity, and rewards. Operational failures return no rewards and do not
15
+ cancel or distort siblings.
17
16
 
18
17
  Custom rollout loops can inherit `Environment` directly. `HarborEnv` follows
19
18
  this path because Harbor owns the complete harness loop. Most Harbor users only