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.
- benchmax-0.2.2.dev0/PKG-INFO +109 -0
- benchmax-0.2.2.dev0/README.md +94 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/pyproject.toml +2 -2
- benchmax-0.2.2.dev0/src/benchmax/auth.py +187 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/bundle.py +29 -55
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/README.md +2 -3
- benchmax-0.2.2.dev0/src/benchmax/envs/base/README.md +80 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/base/__init__.py +1 -1
- benchmax-0.2.2.dev0/src/benchmax/envs/base/dataset.py +80 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/base/env.py +45 -55
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/dataset.py +11 -1
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/environment.py +63 -78
- benchmax-0.2.2.dev0/src/benchmax/envs/harbor/README.md +86 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/bundled_agent.py +9 -27
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/credentials.py +3 -8
- benchmax-0.2.2.dev0/src/benchmax/envs/harbor/dataset.py +303 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/env.py +229 -84
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/types.py +1 -3
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/identity.py +2 -6
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/logging.py +1 -1
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/shared_types.py +24 -7
- benchmax-0.2.2.dev0/src/benchmax/rewards/README.md +75 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/__init__.py +9 -9
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/adaptive.py +3 -9
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/deterministic.py +3 -3
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/diversity.py +5 -4
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/judge.py +10 -34
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/prompts.py +5 -13
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/rubric.py +15 -37
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/rewards/scoring.py +11 -35
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/conftest.py +1 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/bundle/test_artifact.py +3 -12
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/bundle/test_source_capture.py +7 -26
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_base_dataset.py +33 -2
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_base_env_group.py +209 -64
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_contract_types.py +7 -5
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_environment_group.py +109 -44
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/envs/test_example_id.py +0 -1
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/fakes/model_server.py +1 -3
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/harbor/test_bundled_agent.py +12 -24
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/harbor/test_harbor_dataset.py +125 -6
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/harbor/test_harbor_env.py +200 -97
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_adaptive.py +1 -4
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_deterministic.py +7 -5
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_diversity.py +0 -1
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_diversity_env.py +9 -22
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_judge.py +8 -4
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_rubric.py +1 -4
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/rewards/test_rubric_rewards.py +1 -4
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/tests/unit/test_auth.py +27 -2
- benchmax-0.2.0/PKG-INFO +0 -193
- benchmax-0.2.0/README.md +0 -178
- benchmax-0.2.0/src/benchmax/auth.py +0 -109
- benchmax-0.2.0/src/benchmax/envs/base/README.md +0 -74
- benchmax-0.2.0/src/benchmax/envs/base/dataset.py +0 -75
- benchmax-0.2.0/src/benchmax/envs/harbor/README.md +0 -156
- benchmax-0.2.0/src/benchmax/envs/harbor/dataset.py +0 -158
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/.gitignore +0 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/pytest.ini +0 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/__init__.py +2 -2
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/base/openai_types.py +0 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/__init__.py +0 -0
- {benchmax-0.2.0 → benchmax-0.2.2.dev0}/src/benchmax/envs/harbor/dep_check.py +0 -0
- {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.
|
|
3
|
+
version = "0.2.2.dev0"
|
|
4
4
|
description = "Platform-independent runtime for grouped LLM environments"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
authors = [{ name = "
|
|
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
|
|
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
|
|
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) ->
|
|
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
|
|
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
|
|
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
|
|
178
|
-
"runtime version is unknown. Install
|
|
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
|
|
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
|
|
190
|
-
f"but this runtime uses
|
|
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
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
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.
|
|
15
|
-
|
|
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
|