NVIDIA-NeMo / NVIDIA-NeMo/Gym

[VDR][v0.5.0] Public API / extension contract

Open
#2,240 0 comments 0 reactions 1 assignee View on GitHub

@ananthsub is already working on this.

Since Aug 2, 2026.

vdr
Dominant language
Python
Stars
1.2k
Forks
349
Avg merge
1d 21h
Merged PRs (30d)
318

Description

The extension contract for third parties — subclass a base server, implement verify()/run(). Internal naming hygiene is good, but the public boundary is not discoverable: a new author has to read existing servers rather than the base classes to learn the real contract. This is the main blocker for external environment contributions.

Findings: 12 (3 high, 5 medium, 4 low)
Code paths: CONTRIBUTING.md, nemo_gym/__init__.py, nemo_gym/base_resources_server.py, nemo_gym/base_responses_api_agent.py, nemo_gym/server_utils.py, resources_servers/litmus_agent/app.py
Source: DX review Gym-1-20260730170035 · target ref e446e4f4 · 2026-07-30
Tracker: NVIDIA-NeMo/Gym#2214

Sub-issues (critical + high)

Filed individually as sub-issues of this one:

  • 🟠 API-08385826responses() is abstract on the agent base but 8 agents stub it with NotImplementedError
  • 🟠 API-3103951e — Abstract run()/verify() signatures do not describe the real contract
  • 🟠 API-5534f57f — No curated public namespace; every extension imports from deep internal paths

Additional findings in this workstream

Tracked here rather than as separate sub-issues (below the critical/high bar for individual issues, but in scope for this workstream).

  • 🟡 API-c62d9a95ServerClient._build_server_base_url is private-named but is de-facto public API
  • 🟡 API-22cc6e45 — HTTP route versioning is inconsistent across the three server types
  • 🟡 API-d4c1531d — No extension point for wrapping verify(); a server resorts to mutating the route table
  • 🟡 API-2e033acfREVERIFY_MODE contract declared by only 4 of 100 resources servers
  • 🟡 API-ae85c07e — Importing the public package mutates global interpreter state
  • 🔵 API-50e16a94= Body() declaration style differs between the resources base and the agent/model bases
  • 🔵 API-01832ec2 — No documented deprecation or API-stability policy
  • 🔵 API-7373e000 — Server and request-model naming drifts from the dominant convention
  • 🔵 API-cda6750e — Two servers' verify responses are only structurally, not nominally, BaseVerifyResponse
Detail for the findings above
🟡 API-c62d9a95ServerClient._build_server_base_url is private-named but is de-facto public API

Severity: medium Path: nemo_gym/server_utils.py:326 Found by: api-consistency-checker Status: new in this review

The underscore prefix signals internal-only, yet 11 first-party servers and agents call self.server_client._build_server_base_url(...) to resolve a downstream model URL, and the agent base class's own resolve_model_base_url() calls it too. One agent even monkey-patches it on a mock client in tests. Third-party authors copying these patterns take a dependency on a symbol carrying no stability guarantee.

Suggested fix: Promote it to a public ServerClient.server_base_url(server_config) (keeping _build_server_base_url as a deprecated alias via the existing moved_attr_getter shim), and migrate the 11 call sites plus resolve_model_base_url.

+++ Evidence

11 call sites, e.g. resources_servers/xstest/app.py:178, responses_api_agents/codex_agent/app.py:532, claude_code_agent/app.py:317, opencode_agent/app.py:247; base_responses_api_agent.py:133 uses it too

+++

🟡 API-22cc6e45 — HTTP route versioning is inconsistent across the three server types

Severity: medium Path: nemo_gym/base_resources_server.py:135 Found by: api-consistency-checker Status: new in this review

The model server registers every route under /v1/ (/v1/chat/completions, /v1/responses, /v1/messages). The agent server mixes conventions: /v1/responses is versioned while /run and /aggregate_metrics are not. The resources server is entirely unversioned (/verify, /seed_session, /aggregate_metrics, /reverify_mode). Users integrating against the HTTP surface must memorize which endpoints carry the prefix.

Suggested fix: Pick one rule and document it — e.g. /v1/ only for OpenAI/Anthropic-compatible passthrough routes, bare paths for Gym-native control routes — and state it in the reference docs. If Gym-native routes should be versioned, add /v1/ aliases now and keep the bare paths as permanent redirects.

+++ Evidence

model: /v1/chat/completions, /v1/responses, /v1/messages ; agent: /v1/responses, /run, /aggregate_metrics ; resources: /verify, /seed_session, /aggregate_metrics, /reverify_mode

+++

🟡 API-d4c1531d — No extension point for wrapping verify(); a server resorts to mutating the route table

Severity: medium Path: resources_servers/litmus_agent/app.py:653 Found by: api-consistency-checker Status: new in this review

SimpleResourcesServer.setup_webserver() binds /verify directly to self.verify with no pre/post hook. To reap a sandbox after verification, litmus_agent filters the base-registered route out of app.router.routes and re-registers its own handler. This reaches into Starlette internals to work around a missing hook, and would silently break if the base ever registered the route differently.

Suggested fix: Either give SimpleResourcesServer an overridable verify_handler / post-verify hook (or a documented cleanup callback keyed off session teardown), or document overriding verify() itself with a super().verify(body) call as the sanctioned pattern.

+++ Evidence

app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) != "/verify"] ; app.post("/verify")(self._verify_and_cleanup)

+++

🟡 API-2e033acfREVERIFY_MODE contract declared by only 4 of 100 resources servers

Severity: medium Path: nemo_gym/base_resources_server.py:80 Found by: api-consistency-checker Status: new in this review

BaseResourcesServerConfig.REVERIFY_MODE defaults to ReverifyMode.UNKNOWN, and only 4 servers override it. _check_reverify_mode treats UNKNOWN identically to UNSUPPORTED, so 96% of environments trip the reverify guard and require --force. An opt-in class attribute that almost nobody sets is an API that exists but does not function.

Suggested fix: Either make REVERIFY_MODE a required declaration on new servers (enforced by the existing add-verified-flag-style pre-commit hook) or default stateless servers to STATELESS and require only stateful ones to opt out. Also mention it in the environment tutorials, which currently never do.

+++ Evidence

$ grep -rln REVERIFY_MODE resources_servers/*/app.py | wc -l -> 4 ; rollout_reverification.py:508: if mode in (ReverifyMode.UNSUPPORTED, ReverifyMode.UNKNOWN)

+++

🟡 API-ae85c07e — Importing the public package mutates global interpreter state

Severity: medium Path: nemo_gym/__init__.py:150 Found by: api-consistency-checker Status: new in this review

import nemo_gym replaces builtins.print with an always-flushing variant, rewrites sys.path[:] wholesale via _augment_sys_path(), and sets TOKENIZERS_PARALLELISM/HF_HOME/HF_DATASETS_CACHE in os.environ. These are process-global side effects triggered by importing a library, which is surprising for anyone embedding Gym inside a larger application and can silently change unrelated code's behavior.

Suggested fix: Move the builtins patch, sys.path rewrite, and env-var defaults behind an explicit nemo_gym.bootstrap() that the gym CLI and server entrypoints call, leaving a bare import nemo_gym side-effect-free. At minimum, document these effects at the top of the module and gate them on an env var.

+++ Evidence

$ python -c 'import sys; p=print; import nemo_gym; ...' -> print replaced: True -> print_always_flushes ; TOKENIZERS_PARALLELISM: false ; HF_HOME: True

+++

🔵 API-50e16a94= Body() declaration style differs between the resources base and the agent/model bases

Severity: low Path: nemo_gym/base_resources_server.py:171 Found by: api-consistency-checker Status: new in this review

Every abstract method on SimpleResponsesAPIAgent and SimpleResponsesAPIModel declares its body parameter as body: T = Body(), while SimpleResourcesServer.verify() and seed_session() declare a bare body: T. Functionally equivalent under FastAPI, but an author writing both an agent and a resources server sees two conventions for the same thing and has to guess which matters.

Suggested fix: Normalize on one form across all five base classes — = Body() is the more explicit and already the majority — and apply it to verify() and seed_session().

+++ Evidence

verify sig : (self, body: BaseVerifyRequest) -> BaseVerifyResponse ; agent run : (self, body: BaseRunRequest = Body(PydanticUndefined)) -> BaseVerifyResponse

+++

🔵 API-01832ec2 — No documented deprecation or API-stability policy

Severity: low Path: CONTRIBUTING.md:26 Found by: api-consistency-checker Status: new in this review

The mechanics of deprecation are well built — cli/_compat.py provides moved_attr_getter, cli/legacy.py prints migration notices, and config_types.py emits DeprecationWarning for legacy dataset identifiers. But nothing states which symbols are public and stable, how long a deprecated alias survives, or what versioning scheme (__version__ is 0.5.x) implies for breakage. There is no CHANGELOG at the repo root.

Suggested fix: Add a short stability policy to CONTRIBUTING.md or the reference docs: define the public surface (ideally by pointing at the __all__ from API-001), state the minimum deprecation window in releases, and note that everything else is internal. Pair it with a root CHANGELOG.md.

+++ Evidence

CONTRIBUTING.md:26 — "Features and breaking changes: Open an issue to discuss before implementing" (only stability-related line); `ls | grep -i change` -> no results

+++

🔵 API-7373e000 — Server and request-model naming drifts from the dominant convention

Severity: low Path: resources_servers/simpleqa/app.py:231 Found by: api-consistency-checker Status: new in this review

Roughly 90 servers name their class <Name>ResourcesServer, but about 10 use the shorter <Name>Server (SimpleQAServer, AbstentionServer, OmniscienceServer, MultiChallengeServer, InverseIFServer, LongmtEvalServer, ArenaJudgeServer, ...) and two use <Name>Env (BlackjackEnv, ExampleMultiTurnEnv). Several request models also diverge from their directory name, so grepping by env name does not find the type.

Suggested fix: Add a lightweight pre-commit check (alongside the existing add-verified-flag hook) asserting that a resources server's class ends in ResourcesServer and that its request/response models are prefixed consistently with the directory name. Apply to new servers only to avoid a mass rename.

+++ Evidence

class names: SimpleQAServer, AbstentionServer, OmniscienceServer, MultiChallengeServer, BlackjackEnv, ExampleMultiTurnEnv ; dir->type drift: code_gen->CompCodingVerifyRequest, verifif->TuringVIFVerifyRequest

+++

🔵 API-cda6750e — Two servers' verify responses are only structurally, not nominally, BaseVerifyResponse

Severity: low Path: resources_servers/tavily_search/app.py:120 Found by: api-consistency-checker Status: new in this review

TavilySearchVerifyResponse(TavilySearchVerifyRequest, JudgeEvaluation) picks up reward: float from JudgeEvaluation rather than from BaseVerifyResponse, so it is wire-compatible but fails isinstance(x, BaseVerifyResponse). The same pattern appears in browsecomp_advanced_harness. Any future tooling that narrows on the declared base type will silently skip these two servers.

Suggested fix: Change the bases to (TavilySearchVerifyRequest, BaseVerifyResponse, JudgeEvaluation) — or drop reward from JudgeEvaluation and inherit it — so every /verify response is nominally a BaseVerifyResponse.

+++ Evidence

class TavilySearchVerifyResponse(TavilySearchVerifyRequest, JudgeEvaluation):  # JudgeEvaluation(BaseModel) supplies `reward: float` ; 67 other servers subclass BaseVerifyResponse directly

+++


Filed from an automated first-time-developer DX review. Severity reflects impact on a new user: 🔴 critical = blocks usage entirely · 🟠 high = significant friction, workaround required · 🟡 medium = causes confusion · 🔵 low = cosmetic.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.