Repository navigation
feat(stirrup): align the GDPVal sandbox with AA v2 and harden the agent runtime - #3685
Merged
Merged
Conversation
β¦nt runtime Signed-off-by: Serge Panev <spanev@nvidia.com>
This was referenced Sep 24, 2026
agronskiy
approved these changes
Sep 30, 2026
Contributor
|
/ok to test 18c6678 |
muskank7
pushed a commit
to muskank7/Gym
that referenced
this pull request
Sep 30, 2026
β¦quests (NVIDIA-NeMo#3682) ## What does this PR do? Since NVIDIA-NeMo#2456 made `NeMoGymChatCompletionCreateParamsNonStreaming` strict (`extra="forbid"`), every model server's non-streaming `/v1/chat/completions` returns 422 for fields that Gym's own callers send: - `chat_template_kwargs`. Stirrup (`nemo_client.py`), hermes_agent and mini_swe_agent_2 put it in `extra_body`, and the OpenAI SDK sends `extra_body` keys as top-level request fields. hermes_agent sends it on every call by default. - `thinking` and `output_config`. These are Anthropic's reasoning request fields (`{"type": "adaptive"}`, `{"effort": "high"}`), accepted by OpenAI-compatible gateways that front Claude. Any OpenAI-SDK client that passes them in `extra_body` hits the same 422. GDPVal's Claude judge does (NVIDIA-NeMo#3686). Separately, NVIDIA-NeMo#2491 typed the `input_audio` content part with the SDK's `Literal["wav", "mp3"]`, so other formats also return 422. GDPVal's media conversion (NVIDIA-NeMo#3686) sends m4a, flac, ogg, aac and aiff to self-hosted audio judges. Changes: - `nemo_gym/openai_utils.py` - Adds `chat_template_kwargs`, `thinking` and `output_config` to the strict request model as `Optional[Dict[str, Any]]`. The set is exported as `CHAT_REQUEST_PROVIDER_EXTENSION_FIELDS`. Other unknown keys, such as a `thinkng` typo, still return 422. - Adds `NeMoGymInputAudio`, where `format` is any string but still required. The audio content part no longer subclasses the SDK's `ChatCompletionContentPartInputAudioParam`, because a TypedDict subclass can't widen the SDK's `Literal`. - `nemo_gym/chat_streaming.py`: the `stream: true` sanitizer excludes `CHAT_REQUEST_PROVIDER_EXTENSION_FIELDS`, so streaming requests keep main's behaviour (the three fields are dropped, and 200 is returned). Without this, adding the fields to the params model would have started forwarding them on the streaming path. - `responses_api_models/vllm_model/app.py`: adds `forward_request_chat_template_kwargs: bool = False`. vllm_model always removes the request's top-level `chat_template_kwargs` from the body. It merges them only when this flag is on, in this order: configured `chat_template_kwargs`, then the request field, then `metadata.chat_template_kwargs`, with later sources overriding earlier ones. When a non-empty value is dropped, a warning is logged once per server. The flag applies to the chat path only. With `use_completions_api: true` the request field is always dropped (with the same one-time warning). - `fern/versions/latest/pages/model-server/vllm.mdx`: adds the `forward_request_chat_template_kwargs` row and documents the precedence, the completions-path limit and the streaming drop under "Advanced: `chat_template_kwargs`". - Tests: - The chat field-set pin is now SDK fields plus `CHAT_REQUEST_PROVIDER_EXTENSION_FIELDS`, in `test_openai_utils.py` and in vllm_model's `OPENAI_2_44_OPTIONAL_CHAT_FIELDS`. - Open audio formats are accepted, and `format` is still required. - Each extension field rejects values that aren't JSON objects. - The streaming sanitizer drops all three fields, including a non-object `thinking: true`. - openai_model passes `thinking` and `output_config` through, and a typo still returns 422. - vllm_model: the precedence order with forwarding on (no warning); the default drop, with exactly one warning over two requests; no warning for an empty `{}`; `thinking` and `output_config` reach vLLM unchanged; the completions path drops the field and warns. Origin: this work reached the gdpval-devel branch through NVIDIA-NeMo#2103 (open audio part) and NVIDIA-NeMo#3086 (extension fields and the forwarding flag). This PR adds the streaming exclusion, the warning, and the missing tests and docs. The `reasoning_field` config that shared these files on gdpval-devel is split out into NVIDIA-NeMo#3683. Issue: none filed. The 422 regression was found while moving gdpval-devel into main. Dependencies: none. This is NVIDIA-NeMo#3682 of the gdpval-devel promotion series. NVIDIA-NeMo#3683 edits the same three vllm_model and docs files in separate hunks; the two merge cleanly in either order. NVIDIA-NeMo#3686 needs this PR for its judge requests and for five of its tests: with this PR's source files reverted, 4 `test_judge_panel.py` cases and 1 `test_media_conversion.py` audio case fail. NVIDIA-NeMo#3685 needs it at runtime for Stirrup's `chat_template_kwargs`. Open decision for review: the default of `forward_request_chat_template_kwargs`. This PR keeps gdpval-devel's behaviour: the field is accepted and dropped (with a one-time warning) unless forwarding is enabled. The alternative is to forward by default, which changes generation for every caller that sends the field. ## Validation Branch `spanev/gym-chat-provider-fields-v2` (one commit on `1c8261080`), macOS, Python 3.13, `uv venv --python 3.13 .venv && uv sync --extra dev`. - `pytest tests/unit_tests/test_openai_utils.py tests/unit_tests/test_chat_completions_streaming.py tests/unit_tests/test_public_api.py responses_api_models/vllm_model/tests responses_api_models/openai_model/tests -q`: 625 passed (246 + 38 + 61 + 270 + 10). - `pytest responses_api_models/<server>/tests -q`, one server at a time: | Server | Result | |---|---| | azure_openai_model | 3 passed | | inference_provider | 23 passed | | litellm_model | 24 passed | | local_vllm_model_proxy | 2 passed | | openai_model | 10 passed | | switchyard_model | 42 passed, 6 skipped | | vllm_model | 270 passed | | vllm_model_with_compaction | 6 passed | genrm_model and local_vllm_model were not run because they need the `vllm` package. - `pytest tests/unit_tests -q`: 5626 passed, 161 skipped, 60 failed (20 tests and 40 subtests). All failures are in six files that also fail on unmodified main on macOS: `telemetry/test_memory.py`, `test_ci_environment.py`, the `test_cli.py` memory profiler, `test_enroot_provider.py`, `test_opensandbox_compose.py` and `test_super_vllm_launcher.py`. The subtest count varies between runs because `test_super_vllm_launcher.py` subtests are flaky on macOS. - `resources_servers/gdpval` suite: 156 passed. `responses_api_agents/stirrup_agent` suite: 151 passed. Both ran against main plus this PR. - `pre-commit run --files <the 8 changed files>`: all hooks pass. - Ingress probe: direct `model_validate` calls, plus `POST /v1/chat/completions` on vllm_model with a mocked upstream, default config plus `chat_template_kwargs: {"enable_thinking": true}`: | Request | main `1c8261080` | This PR | |---|---|---| | `chat_template_kwargs: {"enable_thinking": false}` | 422 | 200. Upstream receives only the configured `{"enable_thinking": true}` | | `thinking: {"type": "adaptive"}` | 422 | 200 | | `output_config: {"effort": "high"}` | 422 | 200 | | `input_audio` with `format: "m4a"` | 422 | 200 | | `thinkng` (typo, control) | 422 | 422 | - Streaming probe: `stream: true` with all three fields, on openai_model and vllm_model with a mocked upstream. Both return 200 and the upstream receives none of the three fields, the same as main. A non-object value (`thinking: true`) on the streaming path also returns 200, as on main. ## Rollouts **Live GDPVal smoke (final code):** all seven PRs of the series applied together on main `1c8261080`, run with the devel-only SLURM launcher on a 4-task AA-v2 comparison smoke (one 4-GPU policy node, CPU judging): rollout 4/4 tasks saved and finished (1h04m, 0 failure rows), Office preparation 7/7 converted, comparison judging 4/4 tasks judged with 0 imputed and 0 excluded, 3 wins / 1 loss. Per-task outcomes match an earlier gdpval-devel run on the same checkpoint, tasks and references. No rate limiting occurred, so the judge retry budget was not exercised live. ## Compatibility and benchmark impact - Non-streaming requests: the strict chat request model accepts three new optional top-level fields, `chat_template_kwargs`, `thinking` and `output_config`, each a JSON object. Requests that main rejects with 422 because of these fields now pass ingress. - Streaming (`stream: true`) requests: no change. The sanitizer still drops the three fields before validation, so PinchBench's OpenClaw config (which sends `chat_template_kwargs` over streaming) behaves as on main behind every model server. - vllm_model (and the servers that inherit its config: local_vllm_model, genrm_model, local_vllm_model_proxy, vllm_model_with_compaction): drops a request's top-level `chat_template_kwargs` by default and logs one warning per server. Configured `chat_template_kwargs` and `metadata.chat_template_kwargs` behave as before. Turning `forward_request_chat_template_kwargs` on changes generation for callers that send the field, such as Stirrup's `enable_thinking` or hermes_agent's defaults, so re-run baselines if you enable it. - vllm_model forwards `thinking` and `output_config` to the vLLM endpoint unchanged on the chat path (`use_completions_api: false`, the default) and doesn't interpret them. With `use_completions_api: true`, none of the three fields reaches the `/v1/completions` body. - openai_model, inference_provider, switchyard_model and litellm_model forward all three fields to their upstream unchanged on non-streaming requests. An upstream that doesn't know a field now returns its own error instead of Gym's 422. - azure_openai_model passes the body to the OpenAI SDK's `chat.completions.create`, which rejects unknown keyword arguments, so these requests now fail with a 500 before reaching upstream instead of Gym's 422. - `input_audio.format` accepts any string instead of only `wav` or `mp3`, and is still required. `NeMoGymChatCompletionContentPartInputAudioParam` is no longer a subclass of the SDK TypedDict; this affects type checking only. - New module-level names in `nemo_gym.openai_utils` (not re-exported from `nemo_gym`): `NeMoGymInputAudio` and `CHAT_REQUEST_PROVIDER_EXTENSION_FIELDS`. - Benchmark impact: none with default config. Every request main accepts today, streaming or not, sends the same body upstream. Requests main rejects today now succeed; behind vllm_model their `chat_template_kwargs` is ignored. ## Checklist - [ ] I have read the [contributing guidelines](https://github.com/NVIDIA-NeMo/Gym/blob/main/CONTRIBUTING.md). - [x] The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs. - [x] Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable). - [x] Documentation added or updated, or N/A with justification. - [x] Pre-commit checks pass locally (`pre-commit run --all-files`) (so CI lint/format checks pass). Run scoped to the changed files. - [x] New source files include the required Apache-2.0 SPDX header. N/A, no new files. - [x] All commits have DCO sign-off (`git commit -s`) (so the DCO check passes). Signed-off-by: Serge Panev <spanev@nvidia.com>
Contributor
|
π Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/36800414919 |
khazic
pushed a commit
to khazic/Gym-lao
that referenced
this pull request
Oct 1, 2026
β¦IA-NeMo#3686) ## What does this PR do? This is NVIDIA-NeMo#3686 of the series that moves `gdpval-devel` to main, cut by file. It covers GDPVal judging in the resources server, the GDPVal benchmark config and README, the local-judge overlays, and the Stirrup agent's GDPVal task code (`app.py`, `file_reader.py`, `prompts/`). Every file is copied whole from one snapshot: `gdpval-devel` merged with main, plus NVIDIA-NeMo#2046's review fixes and pre-promotion fixes, including the fixes listed at the end of this section. The work comes from devel PRs NVIDIA-NeMo#2103, NVIDIA-NeMo#2331, NVIDIA-NeMo#2338, NVIDIA-NeMo#2588, NVIDIA-NeMo#3086, NVIDIA-NeMo#3121, NVIDIA-NeMo#3131, NVIDIA-NeMo#3184, NVIDIA-NeMo#3335 and NVIDIA-NeMo#3648, and from NVIDIA-NeMo#2046. **Judge transport per provider (comparison mode; NVIDIA-NeMo#2588, NVIDIA-NeMo#3131)** - Each panel member (GPT-5.5, Gemini 3.1 Pro, Claude Opus 4.8) has its own fixed-model `openai_model` proxy. All three default to the same endpoint (`JUDGE_BASE_URL`). - Each member declares how it receives PDFs and rendered Office files (`media_mode`: `native_pdf`, `images_and_text` or `native_pdf_overflow_images`), plus provider limits: native-PDF pages, documents and bytes, image bytes, video count, raster DPI tiers, and a serialized-request cap. - A deterministic preflight removes any member that can't carry a matchup within its limits. If every member is removed for every matchup, the row fails as terminal `transport_ineligible`. - These per-member settings apply to comparison mode only. Rubric mode samples from the same panel but sends every member the server-level `judge_media_mode` (native PDFs by default), with no per-member limits or preflight. - Audio and video capability are separate flags (`handles_audio`, `handles_video`, `on_missing_av_judge`). Video goes to video-capable members. Audio that the routed member can't read is dropped with a warning. - Attachments are bounded per file, per section and per request (raw and encoded bytes), with ZIP member limits and text caps. An attachment left out for size is replaced by a marker the judge sees. - Legacy `.doc`, `.ppt` and `.xls` files are preconverted. When two Office files share a stem, each gets its own `<name>.<ext>.pdf` (NVIDIA-NeMo#2331). A failed OOXML conversion is retried once from a python-docx/python-pptx/openpyxl re-save. **Validity of judge results (NVIDIA-NeMo#2588, NVIDIA-NeMo#3648)** - Rubric: a truncated, scoreless or unparseable reply is flagged `invalid_judge_response` with a `scoring_error` instead of scoring 0 or a partial score. - Structured rubric only (`rubric_scoring_mode: structured`): negative criterion points no longer count toward `max_possible` (NVIDIA-NeMo#3648). - Comparison: the verdict is the last `BOXED[...]` token, a reply with no verdict is dropped instead of counted as a tie, and the prompt ends with a final-verdict reminder. - A request that is too large or overflows the context window fails as permanent. Throttling is retried; rubric retries now also match 502, "bad gateway" and "too many requests". - `strict_comparison_trials` (opt-in) rejects a row unless every planned vote is valid. - Stirrup turns an invalid judge response into a retryable `judge_invalid` failure that keeps the finished deliverable (`reuse_cached_deliverable`), so the retry re-judges it without re-running the policy. - Deliverable conversion runs off the event loop. **Local judges (NVIDIA-NeMo#2046, NVIDIA-NeMo#2103)** - `judge_media_mode: images_and_text` rasterizes PDF and Office pages with PyMuPDF and attaches the extracted text (`media_conversion.py`). `python -m resources_servers.gdpval.inspect_conversion` writes out what an `images_and_text` judge receives. - Overlays for a Kimi-K2.6 judge that gym spawns (`gdpval_kimi_local_judge.yaml`, `local_vllm_model/configs/moonshotai/Kimi-K2.6.yaml`) and for a self-hosted MiniMax-M3 endpoint (`gdpval_minimax_selfhosted_judge.yaml`). **Inputs (NVIDIA-NeMo#3131, NVIDIA-NeMo#3335)** - `judge_reference_files_recursive` (opt-in) includes nested files under a task's `reference_files/`. `judge_reference_files_from_eval` (opt-in) reads `reference_files/` from the eval task directory instead of each reference model's directory. - `count_eval_missing_as_loss` with `missing_eval_task_ids` (opt-in, multi-stage `stage_index` 1 only) scores a listed task with no eval deliverable as a loss. The resources server and the Stirrup agent must both set them. - `setup_libreoffice.py` skips the reinstall when LibreOffice's `javaldx` works, and the apt timeout is 1800 s. **Stirrup GDPVal task code (NVIDIA-NeMo#2338, NVIDIA-NeMo#2588, NVIDIA-NeMo#3121, NVIDIA-NeMo#3131, NVIDIA-NeMo#3184)** - New agent config fields, defaulting to the old behaviour: `context_window_tokens` (passed as the client's `max_tokens`), `min_completion_tokens`, `prompt_estimator_truncate_history_thinking` and `truncation_recovery` (passed to NVIDIA-NeMo#3685's client), and `min_compaction_summary_words` (passed to NVIDIA-NeMo#3685's `NeMoAgent`). - The GDPVal prompts (`prompts/`, moved here from NVIDIA-NeMo#3685 so they land with the code that builds their reference paths) name the registered `code_exec` tool instead of `run_shell`, list only packages the AA v2 sandbox installs, and state the `/root` working directory, the 5-minute command limit and that shell state does not persist. Reference files are listed relative to the sandbox working directory, without the `gdpval_ref_files_<id>/` prefix. - The verify cache is keyed by the row's namespace. The file reader bounds text extraction, sniffs unknown extensions for text, lists archive contents, and degrades without LibreOffice. **Benchmark config** - Starts the three member proxies alongside `gdpval_judge_model`, each with `max_http_attempts: 1800`, and sets each member's media limits. - Claude judge: `thinking: {type: adaptive}` with `output_config.effort: high` and no temperature. - `judge_pdf_max_pages: 1000`. - Stirrup: `min_completion_tokens: 8192`, `context_window_tokens: 262144`, `min_compaction_summary_words: 50`, `truncation_recovery: true`. - NVIDIA-NeMo#3684's `retry_terminal_timeouts` and `retry_invalid_judge_responses`, both `true`. **Fixes made for this PR** - The removed `handles_audio_video` member key is now migrated to both new flags with a deprecation warning; combining it with either new flag is an error. Before, it was silently ignored. - `inspect_conversion.py` crashed at start (`ImportError` on `IGNORE_FILES`). It and `comparison.py` now read `IGNORE_FILES` from `nemo_gym.deliverables`. - README and config comments corrected: per-member proxies, Claude's adaptive thinking, the comparison-only scope of per-member transport, how the page cap works in comparison mode, the judge timeout and retry layers, and the Kimi overlay. The README now also documents the judge input options, attachment environment variables and aggregate metrics. - New tests for the overlays, the comparison retry loop, truncated rubric JSON, the inspector, and the Stirrup client and agent arguments. - The NVIDIA-NeMo#3666 media-preparation helpers (`media.py`, and `check_reference_inputs` and `office` moved into it from `preconvert.py`) have no caller in this series and stay on `gdpval-devel`. Issue: none. Most of this was reviewed in the devel PRs above; the fixes in the last group are new here. Related: NVIDIA-NeMo#2046, whose changes and review fixes are included. **Depends on NVIDIA-NeMo#3682, NVIDIA-NeMo#3685 and NVIDIA-NeMo#3684; merge after them.** It does not need NVIDIA-NeMo#3681 or NVIDIA-NeMo#3683. - NVIDIA-NeMo#3684: `comparison.py` and Stirrup's `file_reader.py` import `nemo_gym/deliverables.py` at module scope, so both suites fail to collect without it. The benchmark config sets C3's two retry flags, which main's config silently ignores. C3's rollout collection also carries `reuse_cached_deliverable` onto the retried row; without it a `judge_invalid` retry re-runs the policy. - NVIDIA-NeMo#3682: the Claude judge sends `thinking` and `output_config`, which main's strict chat schema rejects with a 422. Main's audio part also accepts only wav and mp3. - NVIDIA-NeMo#3685: Stirrup's `app.py` passes the new arguments to NVIDIA-NeMo#3685's client and agent; main's client raises `TypeError: ... unexpected keyword argument 'min_completion_tokens'`. The prompt tests read NVIDIA-NeMo#3685's vendored sandbox manifests. - Open on NVIDIA-NeMo#3682: under its current default (`forward_request_chat_template_kwargs: false`), `vllm_model` drops request `chat_template_kwargs`. The thinking-off half of `truncation_recovery` then does not reach a policy served by `vllm_model`; only the one-turn instruction applies. This config does not set the flag. ## Validation macOS, Python 3.13. Each server suite ran in a venv built from that server's `requirements.txt`, with `PYTHONPATH` set to the tree under test. ```bash cd resources_servers/gdpval && python -m pytest tests -q cd responses_api_agents/stirrup_agent && python -m pytest tests -q python -m pytest tests/unit_tests/test_rollout_collection.py tests/unit_tests/test_openai_utils.py tests/unit_tests/test_deliverables.py -q ``` | Tree | GDPVal | Stirrup | |---|---|---| | main `1c8261080` + NVIDIA-NeMo#3681, C2, C3, S and this PR | 414 passed | 321 passed, 2 skipped | | Same, with `cryptography` not importable | 411 passed, 3 skipped (`test_media_conversion` text extraction) | β | | Full series, including NVIDIA-NeMo#3687 | 603 passed | 321 passed, 2 skipped | The three core unit files: 507 passed on the first tree. Each dependency was confirmed by removing one PR's files: | Removed | GDPVal | Stirrup | |---|---|---| | NVIDIA-NeMo#3681 | no change | no change | | NVIDIA-NeMo#3682 | 5 failed: 4 cases of `test_benchmark_panel_reaches_intended_upstream_models_and_credentials`, and `test_every_supported_audio_extension_passes_proxy_ingress` | no change | | NVIDIA-NeMo#3684 | collection errors (`No module named 'nemo_gym.deliverables'`) | collection errors, same cause | | NVIDIA-NeMo#3685 | no change | 5 failed: 4 prompt tests that read NVIDIA-NeMo#3685's manifests, and `test_runtime_knobs_reach_the_client_and_agent` (the `TypeError` above) | | This PR alone | collection errors, same cause | collection errors, same cause | `pre-commit run --files` on this PR's files passed with no changes; the checklist item below is scoped the same way. `uvx ruff@0.9.9 format --check` and `check` are clean on all 44 files, and new files carry the SPDX header. ## Rollouts **Live GDPVal smoke (final code):** all seven PRs of the series applied together on main `1c8261080`, run with the devel-only SLURM launcher on a 4-task AA-v2 comparison smoke (one 4-GPU policy node, CPU judging): rollout 4/4 tasks saved and finished (1h04m, 0 failure rows), Office preparation 7/7 converted, comparison judging 4/4 tasks judged with 0 imputed and 0 excluded, 3 wins / 1 loss. Per-task outcomes match an earlier gdpval-devel run on the same checkpoint, tasks and references. No rate limiting occurred, so the judge retry budget was not exercised live. This PR stays a draft until NVIDIA-NeMo#3682, NVIDIA-NeMo#3685 and NVIDIA-NeMo#3684 merge. ## Compatibility and benchmark impact GDPVal scores after this PR are not comparable with main's. Re-baseline rubric runs and comparison/ELO runs. **Configuration** - Panel members: `handles_audio_video` is replaced by `handles_audio` and `handles_video` (the old key is migrated with a warning). Member names must resolve uniquely, or verify raises `ValueError`. A `native_pdf_overflow_images` member needs `max_native_pdf_pages` and `max_native_pdf_bytes_per_document`. - New resources-server fields (defaults): `judge_media_mode` (`native_pdf`), `judge_pdf_render_dpi` (144), `judge_pdf_max_pages` (50; the benchmark sets 1000), `judge_pdf_include_text` (true), `judge_max_images_per_request` (450), `judge_handles_audio` / `judge_handles_video` (false), `on_missing_av_judge` (`warn`), `judge_reference_files_recursive` / `judge_reference_files_from_eval` (false), `strict_comparison_trials` (false), `count_eval_missing_as_loss` (false), `missing_eval_task_ids` (`[]`), and the per-member transport fields (unset means no limit). - In comparison mode a raster judge renders up to `max(judge_pdf_max_pages, judge_max_images_per_request)` pages per file, and is not used for a matchup whose pages and images exceed `judge_max_images_per_request`. The overlays' `judge_pdf_max_pages: 30` therefore bounds rubric mode only. - New environment variables: `GDPVAL_JUDGE_REQUEST_TIMEOUT_SECONDS` (comparison default goes from 120 s to 300 s; rubric is 1800 s, where the SDK's 600 s with two retries applied before), `GDPVAL_MAX_FILE_BYTES_FOR_JUDGE` (250 MiB, as before, now overridable), the five section/total attachment and request limits, `JUDGE_GPT_MODEL`, `JUDGE_GEMINI_MODEL`, `JUDGE_CLAUDE_MODEL`, `JUDGE_GEMINI_API_KEY` (falls back to `JUDGE_API_KEY`) and `GDPVAL_GEMINI_MAX_CONCURRENT_REQUESTS` (2). - Judge SDK clients use `max_retries=0`; GDPVal's own loops and each proxy's `max_http_attempts` retry instead. `max_http_attempts: 1800` gives about 15 minutes of retries at 0.5 s per request. Main's retry set also covers 404, 408 and 500, so a persistent 404 (such as an unknown model route) takes the whole window to surface. A comparison call times out after 300 s, is not retried, and time queued behind the Gemini proxy's 2-request limit counts toward it; the proxy can keep retrying an abandoned call until its window closes. The README documents how to lower the window per proxy. - The Claude proxy's `extra_body` sends `timeout: 900` and `request_timeout: 900` in every request, as gateway-side keys for the default endpoint. Endpoints that reject unknown fields need them removed. - The benchmark config starts `gdpval_gpt55_judge_model`, `gdpval_gemini31_judge_model` and `gdpval_claude48_judge_model` alongside `gdpval_judge_model`. With `retry_terminal_timeouts: true`, `reference_missing`, `eval_missing` and `transport_ineligible` are terminal within a run and re-checked on `--resume` (NVIDIA-NeMo#3684). **Changes that move GDPVal scores** - Rubric: invalid judge replies no longer score 0 or a partial score. The resources server's rubric `aggregate_metrics` leaves `invalid_judge_response` rows out of the mean for any agent, and adds `rubric/aggregate_rows_total`, `rubric/aggregate_rows_included`, `rubric/legacy_invalid_rows_excluded` and `rubric/aggregate_rows_included_fraction`. - Structured rubric: negative points are excluded from `max_possible`. Binary mode is unchanged. - Comparison verdicts: last `BOXED[...]` token, no-verdict replies dropped, final-verdict reminder in the prompt. `eval_missing` (no `finish_params.json`) was scored as a loss; it is now a terminal failure in the sidecar, like `reference_missing` and `transport_ineligible`. - Comparison payloads: an Office file's rendered PDF is attached once (main attached it under both names). Office files with no render and file types the judge can't read are left out instead of sent as filename stubs. Every rendered `.xlsx` also carries a structured-cells text block (formulas and cached values, up to 120k characters). Each member sees PDFs in its own representation and within its own limits. - Comparison aggregates: when rows carry `expected_final_stage_index` (written by NVIDIA-NeMo#3687), the headline `comparison/eval_elo` comes from that stage. If it is missing, incomplete or unfit, the headline is withheld (`comparison/final_stage_degraded=1`) rather than taken from another stage; rows without the field keep the last-stage headline. New keys: `comparison/stage_<k>/{judged_tasks,judged_votes,imputed_loss_tasks,imputed_loss_votes}`, `comparison/headline_stage_index`, `comparison/final_stage_*` and `comparison/expected_final_stage_*`. `comparison/win_rate` is omitted when no vote was judged. `judge_response` gains `total_invalid` and a per-judge `invalid_count`. - Claude judge: adaptive thinking with high effort and no temperature (was `thinking: enabled`). - Policy (set in the GDPVal config): `min_completion_tokens: 8192` (the code default stays 1024), `min_compaction_summary_words: 50` and `truncation_recovery: true`. The rewritten prompt names the real tool, package set and working directory, and its reference-file paths exist in the sandbox. **Every `stirrup_agent` config** - `timeout_exceeded` is no longer stamped terminal; on `--resume` it is retried up to `NEMO_GYM_MAX_ROLLOUT_ATTEMPTS`. - A verify result with `invalid_judge_response` becomes a failure instead of a scored row: retryable `judge_invalid`, or terminal `permanent` when the server sets `invalid_judge_retryable: false`. - A `/verify` error whose text matches GDPVal's permanent markers (413, "maximum context length", "too many tokens", "input is too long" and similar) is stamped terminal `permanent` and not retried, unless a throttle marker is also present. - The request's `max_output_tokens` now caps completion tokens; the context window comes from `context_window_tokens` (default 262144). - Failure rows after `/verify` carry the policy response plus `deliverables_dir`, `elapsed_seconds` and `_ng_timeout_inflight`. `judge_only` and `rerun_incomplete` fall back to a flat `task_<id>/` directory. - Stirrup now imports `resources_servers.gdpval` (`scoring`, `judge_panel`, `preconvert`, `media_conversion`), while the GDPVal server already imports Stirrup's file reader. **Known limitations** - `requirements.txt` lists `cryptography>=42.0`, but the root `pyproject.toml` `exclude-dependencies` drops it from server venvs, and an `overrides.txt` does not bring it back. Without it, pdfminer can't load: the `images_and_text` text copy is empty (a warning is logged), 3 `test_media_conversion` tests skip, and rubric `read_deliverable_files` returns an error string for PDFs (also true on main). The fix is a change to the root `pyproject.toml`. - Rubric judge sampling is seeded per task, so a `judge_invalid` retry draws the same member. A provider that rejects a task's payload in rubric mode fails every attempt until the attempt limit. - ZIP members skipped by the archive limits are logged but not marked in the judge request. - The README's multi-stage sections describe `partial_completion` and other behaviour that lands with NVIDIA-NeMo#3687. ## Checklist - [x] I have read the [contributing guidelines](https://github.com/NVIDIA-NeMo/Gym/blob/main/CONTRIBUTING.md). - [x] The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs. - [x] Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable). - [x] Documentation added or updated, or N/A with justification. - [x] Pre-commit checks pass locally (scoped: `pre-commit run --files` on this PR's files). - [x] New source files include the required Apache-2.0 SPDX header. - [x] All commits have DCO sign-off (`git commit -s`) (so the DCO check passes). Signed-off-by: Serge Panev <spanev@nvidia.com> Co-authored-by: Marta Stepniewska-Dziubinska <marta-sd@users.noreply.github.com>
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
This PR moves the Stirrup sandbox and runtime work from
gdpval-develto main. On devel it landed as #2338, #2395, #2588, #3086, #3121 and #3333 (sandbox part). It is one of a series of PRs cut by file from a single tested devel snapshot. It touchesresponses_api_agents/stirrup_agent/and the GDPVal sandbox build script innemotron_recipes/. The GDPVal prompt rewrite that describes this sandbox is in #3686, not here.Sandbox (#3333)
containers/gdpval.defnow builds the published GDPval-AA v2 environment:debian:trixiewith Debian's Python 3.13.5. The build fails on any other interpreter version.gdpval_aa_v2_arm64_exclusions.txt. Any other pin that fails to install fails the build.verify_gdpval_sandbox.pyis staged into the image. It checks a built.sifagainst both manifests, then runs the toolchain end to end.%filespaths, so it has to be built fromcontainers/. The README command is updated for that.nemotron_recipes/.../build-gdpval-sif.shnow builds from the definition's directory, where before it built from the repo root, which fails with this definition._build_command_scriptis pulled out ofApptainerCodeExecToolProvider._execwith no behaviour change. A test runs two provider-built commands in one bash and checks that only files persist between calls.Model client and agent (#2338, #3121)
DynamicMaxTokensChatCompletionsClient:max_completion_tokens. A prompt with no room left raises Stirrup'sContextOverflowErrorbefore the request is sent.min_completion_tokens, default 1024 (the old constant).prompt_estimator_truncate_history_thinking, defaultNone. It only affects the estimate.truncation_recovery, defaultFalse. When on, a turn that uses its whole budget without a schema-valid tool call is followed by one turn that requests thinking off and adds a one-time instruction. The instruction is not recorded in the trajectory.NeMoAgent.summarize_messages:<think>block from compaction summaries.min_compaction_summary_wordsargument, default 1.Provider payload and trajectories (#2395, #2588, #3086)
to_provider_openai_messagessends only the standard OpenAI message keys:reasoning_content,thinking_blocksandmetadataon assistant messages, flattool_call_id/name/arguments/signaturefields on tool calls, andnameon tool messages.<tool_call>/<function=...>blocks are removed from the provider copy of assistant text. This applies only to assistant turns with no parsedtool_calls, and an unclosed block is stripped to the end of the message. The recorded history keeps them.convert_stirrup_history_to_output_itemsmakescall_idunique within a response when the server reuses ids across turns (e.g.code_exec:0). Call/output pairing is preserved, and trajectories without repeated ids are unchanged.web_fetch:The new client and agent options are documented in the Stirrup README as constructor arguments. The agent config keys that set them come with #3686, which owns
stirrup_agent/app.py.Issue: none. This promotes work that was already reviewed and merged on
gdpval-devel.Dependencies:
chat_template_kwargsthat Stirrup's client sends on every call. Running Stirrup against a Gym vLLM model server therefore needs fix(openai-utils): accept provider extension fields in strict chat requestsΒ #3682.forward_request_chat_template_kwargs: false), the model server accepts requestchat_template_kwargsand drops them. That means Stirrup'senable_thinking, and the thinking-off half oftruncation_recovery, reach vLLM only when the policy model server setsforward_request_chat_template_kwargs: true. The GDPVal config in feat(gdpval): harden judging with local judges and media inputsΒ #3686 doesn't set it, so there only the recovery instruction takes effect. The default is still open on fix(openai-utils): accept provider extension fields in strict chat requestsΒ #3682.code_exec, no shell state between calls, the 5-minute limit, the AA v2 package summary). It also brings the reference-file path fix that prompt relies on, and the tests that tie the prompt to this sandbox's manifests.context_window_tokens,min_completion_tokens,prompt_estimator_truncate_history_thinking,min_compaction_summary_wordsandtruncation_recovery.run_shelland packages the image doesn't have (torch, keras, jax, dlib). Main's current image doesn't have them either. The new options keep their defaults, and the context window still comes from the request'smax_output_tokens(fallback 262144).Validation
Main
1c8261080+ only this PR's 17 files, macOS, Python 3.13, per-server venvs:cd responses_api_agents/stirrup_agent && python -m pytest tests -q: 224 passed, 2 skipped. The skips need GNUtimeoutor a real Apptainer image (GDPVAL_CONTAINER_PATH).cd resources_servers/gdpval && python -m pytest tests -q(gdpval imports Stirrup): 156 passed.With the rest of the series applied: Stirrup 319 passed / 2 skipped, GDPVal 564 passed. The 12 prompt-content tests that moved to feat(gdpval): harden judging with local judges and media inputsΒ #3686 fail against main's current prompt, which is why they live there.
pre-commit run --all-files: all hooks pass, no files modified.Schema probe against main's
NeMoGymChatCompletionCreateParamsNonStreaming:to_openai_messagesto_provider_openai_messagesAdding
chat_template_kwargsis still rejected until fix(openai-utils): accept provider extension fields in strict chat requestsΒ #3682.Build script: I ran it with a stub
apptainerthat checks every%filessource resolves from the build directory.gdpval_aa_v2_python_requirements.txtis not found from the repo root.Container: built and verified with
verify_gdpval_sandbox.pyin [GDPVal] Align the sandbox and prompt to GDPval-AA v2Β #3333. The image was not rebuilt for this PR, and only comment lines undercontainers/have changed since.Core unit tests: not run. This PR changes nothing under
nemo_gym/,scripts/ortests/.Rollouts
Live GDPVal smoke (final code): all seven PRs of the series applied together on main
1c8261080, run with the devel-only SLURM launcher on a 4-task AA-v2 comparison smoke (one 4-GPU policy node, CPU judging): rollout 4/4 tasks saved and finished (1h04m, 0 failure rows), Office preparation 7/7 converted, comparison judging 4/4 tasks judged with 0 imputed and 0 excluded, 3 wins / 1 loss. Per-task outcomes match an earlier gdpval-devel run on the same checkpoint, tasks and references. No rate limiting occurred, so the judge retry budget was not exercised live.Compatibility and benchmark impact
.sifimages keep working.containers/(cd responses_api_agents/stirrup_agent/containers && apptainer build gdpval.sif gdpval.def) or with the recipes script. A build started from the repo root cannot find the%filesinputs.python:3.13-bookwormtodebian:trixie, with Python fixed at 3.13.5.unaris dropped (it isn't in the AA v2 closure), and three packages move to their trixie names.espeak-ng, and the per-command limit is 5 minutes (AA uses 10).model_idset and a template that renders,max_completion_tokenscan drop below 1024 near the context limit, and a full context raisesContextOverflowErrorbefore the request is sent.model_id, or when tokenization fails, the estimate is larger for every history, most for tool-heavy ones. Budgets shrink only near the context limit, and the 1024 floor still applies.max_completion_tokens_cap,min_completion_tokensandmin_compaction_summary_wordsbelow 1 now raiseValueError.truncation_recoveryis off._unwind_context_overflow,_active_tools,_logger). They are stable under the existingstirrup>=0.1.12,<0.2pin, but a Stirrup minor release may need a follow-up.call_idgets a#nsuffix from its second occurrence onward.web_fetch:success=Falsewith an error. Before, it returned an empty body.truncation_recoveryis off by default. When on, the recovery instruction is appended as a user message, so the provider can see two user messages in a row.min_completion_tokens: 8192andmin_compaction_summary_words: 50. Other Stirrup users are unaffected.StirrupAgentWrapperConfigaccepts extra keys and ignores them.Checklist
pre-commit run --all-files) (so CI lint/format checks pass).git commit -s) (so the DCO check passes).