From bc6bb6e8f17231c019f483a7685bf4842a88cf97 Mon Sep 17 00:00:00 2001 From: Sandy Chapman Date: Mon, 17 Aug 2026 10:34:38 -0300 Subject: [PATCH] refactor(evaluator)!: rename env_overrides to hydra_params, add env_vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `env_overrides` read as "environment variables" to everyone who met it. It is not: the "env" is Gym's *environment* (the resources-server), and the values become Hydra config overrides — `{'a': {'b': 1}}` flattens to `++a.b=1`. The name is now `hydra_params`, which says what it does. That frees `env` to mean what people assumed, and the gap it leaves is real. Some Gym environments are configurable only through the OS environment: `wmt_translation` reads `WMT_TRANSLATION_COMET_PY_CACHE` for its model-cache root and otherwise defaults to `/opt/Gym/.cache/comet-python`, a path that exists only inside NVIDIA's container image, so on any other machine it fails with `PermissionError: /opt/Gym` before the GPU requirement is even reached. Until now the only way to set that was to export it before invoking the runner, which makes a property of the *environment* into a property of whoever happened to launch the run — and a job spec executed elsewhere has no ambient environment to inherit from at all. The new `env_vars` field carries it in the config instead. Precedence is this process's environment, then the Ray uv-hook default, then `env_vars`. Explicit config wins over both, including over the Ray setting: that default exists to make Gym work rather than as an invariant, and someone debugging that hook needs a way to put it back. `_gym_invocation_env` exists so this is assertable without starting Ray. `env_vars` is redacted in `RunnerInfo.config` on the same rules as `hydra_params`, and needs it more: an environment variable is the conventional way to hand a process an API key, so a caller doing the obvious thing would otherwise write one into the run bundle. Verified the existing markers catch `OPENAI_API_KEY`, `HF_TOKEN`, `AWS_SECRET_ACCESS_KEY` and `DB_PASSWORD` while leaving `WMT_TRANSLATION_COMET_PY_CACHE` and `HTTPS_PROXY` verbatim. BREAKING CHANGE: `env_overrides` is renamed to `hydra_params` on both `GymRuntimeConfig` and the Gym runner-target job spec, so it changes the REST contract in plugins/nemo-evaluator/openapi/openapi.yaml. The field landed in main one day ago (#1257) and has no known callers. Signed-off-by: Sandy Chapman Rebased onto main after #1295 split `gym_runtime.py` into the `gym/` package. Re-applied on the new layout rather than resolving a delete/modify conflict against a file that no longer exists. One placement changed as a result: `_gym_invocation_env` now lives in `gym/process.py` rather than beside the runner. The environment a CLI is invoked with is part of how it is run, which is what that module is for, and it keeps the runtime module orchestration-only. It is `process.py`'s first dependency on `config.py`, which stays acyclic. Signed-off-by: Sandy Chapman --- .../nemo_evaluator_sdk/examples/gym/README.md | 2 +- .../agent_eval/runtimes/gym/config.py | 30 ++++++++----- .../agent_eval/runtimes/gym/process.py | 23 ++++++++++ .../agent_eval/runtimes/gym/runtime.py | 15 ++++--- .../test_gym_environment_coverage.py | 16 +++---- .../tests/agent_eval/test_gym_runtime.py | 31 ++++++++++++- .../tests/agent_eval/test_run_metadata.py | 44 ++++++++++++++++--- plugins/nemo-evaluator/openapi/openapi.yaml | 21 +++++++-- .../src/nemo_evaluator/jobs/agent_evaluate.py | 3 +- .../src/nemo_evaluator/jobs/agent_spec.py | 14 ++++-- .../tests/integration/conftest.py | 14 +++--- .../tests/test_agent_evaluate.py | 4 +- .../agent_eval/runtimes/gym/config.py | 30 ++++++++----- .../agent_eval/runtimes/gym/process.py | 23 ++++++++++ .../agent_eval/runtimes/gym/runtime.py | 15 ++++--- 15 files changed, 221 insertions(+), 64 deletions(-) diff --git a/packages/nemo_evaluator_sdk/examples/gym/README.md b/packages/nemo_evaluator_sdk/examples/gym/README.md index 0189e03eb3..b2b4c0c74d 100644 --- a/packages/nemo_evaluator_sdk/examples/gym/README.md +++ b/packages/nemo_evaluator_sdk/examples/gym/README.md @@ -42,7 +42,7 @@ Working from a Gym checkout also makes its components take precedence over the p Useful flags: `--resources-server`, `--agent`, `--model-type` (`inference_provider` for OpenAI-compatible **chat** endpoints; `openai_model` uses the OpenAI **Responses API** and 500s against chat-only endpoints), `--num-repeats`, `--dataset`, `--output-dir`. -For the full set of knobs the underlying `gym env start` / `gym eval run` commands accept, see the [NeMo Gym documentation](https://github.com/NVIDIA-NeMo/Gym). Anything `GymRuntimeConfig` does not expose as a field can be passed through with its `env_overrides` escape hatch — nested data such as `{"model": {"temperature": 0.7}}`, flattened to Hydra's override grammar and applied to `gym env start`. +For the full set of knobs the underlying `gym env start` / `gym eval run` commands accept, see the [NeMo Gym documentation](https://github.com/NVIDIA-NeMo/Gym). Anything `GymRuntimeConfig` does not expose as a field can be passed through with its `hydra_params` escape hatch — nested data such as `{"model": {"temperature": 0.7}}`, flattened to Hydra's override grammar and applied to `gym env start`. Each run writes its bundle to a fresh temporary directory by default. Pass `--output-dir` to choose one, but give every run its own: the runner refuses to reuse a directory that already holds Gym rollout output (Gym appends to its failures sidecar, so reusing one would mix runs) and raises rather than clearing a prior run's results. diff --git a/packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym/config.py b/packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym/config.py index 21a3dabecd..200cd41884 100644 --- a/packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym/config.py +++ b/packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym/config.py @@ -128,10 +128,10 @@ def _flatten_overrides(overrides: Mapping[str, Any], _prefix: str = "") -> list[ return arguments -def _redact_env_overrides(overrides: Mapping[str, Any], _prefix: str = "") -> dict[str, Any]: +def _redact_hydra_params(overrides: Mapping[str, Any], _prefix: str = "") -> dict[str, Any]: """Redact credential-looking values from overrides before they are recorded as provenance. - ``env_overrides`` is a free-form escape hatch forwarded to Gym, so nothing stops a caller passing + ``hydra_params`` is a free-form escape hatch forwarded to Gym, so nothing stops a caller passing ``{"model": {"api_key": "sk-..."}}``. ``RunnerInfo.config`` is persisted into the run bundle, so a value that looks like a credential must not be written there. @@ -147,7 +147,7 @@ def _redact_env_overrides(overrides: Mapping[str, Any], _prefix: str = "") -> di for key, value in overrides.items(): path = f"{_prefix}{key}" if isinstance(value, Mapping): - redacted[key] = _redact_env_overrides(value, f"{path}.") + redacted[key] = _redact_hydra_params(value, f"{path}.") elif any(marker in path.casefold() for marker in _SECRET_KEY_MARKERS): redacted[key] = _REDACTED elif isinstance(value, (list, tuple)): @@ -158,9 +158,9 @@ def _redact_env_overrides(overrides: Mapping[str, Any], _prefix: str = "") -> di def _redact_list_item(item: Any, path: str) -> Any: - """Redact inside one element of a list-valued override. See :func:`_redact_env_overrides`.""" + """Redact inside one element of a list-valued override. See :func:`_redact_hydra_params`.""" if isinstance(item, Mapping): - return _redact_env_overrides(item, f"{path}.") + return _redact_hydra_params(item, f"{path}.") if isinstance(item, (list, tuple)): return [_redact_list_item(nested, path) for nested in item] return item @@ -186,11 +186,11 @@ def _selection_args(config: GymRuntimeConfig, work_dir: Path) -> list[str]: # env we're running. Assumes the agent config's top-level key equals the agent name (the # simple_agent convention) *and* that the resources-server is registered under the # environment's own name — not universally true, so self-contained or differently-named - # servers set bind_resources_server=False and bind themselves via env_overrides. + # servers set bind_resources_server=False and bind themselves via hydra_params. selection.append( f"+{config.agent}.responses_api_agents.{config.agent}.resources_server.name={config.resources_server}" ) - selection.extend(_flatten_overrides(config.env_overrides)) + selection.extend(_flatten_overrides(config.hydra_params)) # Gym is a Hydra app, so each invocation writes a timestamped run directory — by default # `outputs//