Skip to content

Commit 23395ff

Browse files
bxyu-nvidialbliii
authored andcommitted
Bump OpenAI version to 2.6.1; improve dependency constrain resolution (#255)
Signed-off-by: Brian Yu <bxyu@nvidia.com>
1 parent 114726a commit 23395ff

File tree

6 files changed

+78
-64
lines changed

6 files changed

+78
-64
lines changed

nemo_gym/cli.py

Lines changed: 13 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414
import asyncio
1515
import json
1616
import shlex
17-
import subprocess
1817
import tomllib
1918
from glob import glob
2019
from os import environ, makedirs
2120
from os.path import exists
2221
from pathlib import Path
23-
from platform import python_version
2422
from subprocess import Popen
2523
from threading import Thread
2624
from time import sleep
@@ -29,7 +27,7 @@
2927
import rich
3028
import uvicorn
3129
from devtools import pprint
32-
from omegaconf import DictConfig, OmegaConf, open_dict
30+
from omegaconf import DictConfig, OmegaConf
3331
from pydantic import BaseModel, Field
3432
from tqdm.auto import tqdm
3533

@@ -40,6 +38,7 @@
4038
NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME,
4139
NEMO_GYM_CONFIG_PATH_ENV_VAR_NAME,
4240
NEMO_GYM_RESERVED_TOP_LEVEL_KEYS,
41+
PYTHON_VERSION_KEY_NAME,
4342
GlobalConfigDictParserConfig,
4443
get_global_config_dict,
4544
)
@@ -52,45 +51,13 @@
5251
)
5352

5453

55-
def _capture_head_server_dependencies(global_config_dict: DictConfig) -> None: # pragma: no cover
56-
"""
57-
Capture head server dependencies and store it in the global config dict.
58-
These dependencies are used as constraints to ensure that other servers use the same dependency versions as the head server.
59-
Note: This function will modify the global config dict - update `head_server_deps`
60-
"""
61-
62-
try:
63-
result = subprocess.run(
64-
["uv", "pip", "freeze", "--exclude-editable"],
65-
capture_output=True,
66-
text=True,
67-
check=True,
68-
)
69-
head_server_deps = result.stdout
70-
except Exception as e:
71-
print(f"Warning: Could not capture head server dependencies: {e}")
72-
head_server_deps = None
73-
74-
relevant_head_server_deps_list = []
75-
for dep_line in head_server_deps.splitlines():
76-
if "ray" in dep_line:
77-
# The ray version is very sensitive. The children ray versions must exactly match those of the parent ray.
78-
relevant_head_server_deps_list.append(dep_line)
79-
80-
relevant_head_server_deps = "\n".join(relevant_head_server_deps_list)
81-
82-
with open_dict(global_config_dict):
83-
global_config_dict[HEAD_SERVER_DEPS_KEY_NAME] = relevant_head_server_deps
84-
85-
86-
def _setup_env_command(dir_path: Path, head_server_deps: Optional[str] = None) -> str: # pragma: no cover
54+
def _setup_env_command(dir_path: Path, global_config_dict: DictConfig) -> str: # pragma: no cover
8755
install_cmd = "uv pip install -r requirements.txt"
88-
if head_server_deps:
89-
install_cmd += f" --constraint <(cat << 'EOF'\n{head_server_deps}\nEOF\n)"
56+
head_server_deps = global_config_dict[HEAD_SERVER_DEPS_KEY_NAME]
57+
install_cmd += " " + " ".join(head_server_deps)
9058

91-
head_server_python_version = python_version()
9259
return f"""cd {dir_path} \\
93-
&& uv venv --allow-existing --python {head_server_python_version} \\
60+
&& uv venv --allow-existing --python {global_config_dict[PYTHON_VERSION_KEY_NAME]} \\
9461
&& source .venv/bin/activate \\
9562
&& {install_cmd} \\
9663
"""
@@ -153,10 +120,6 @@ class RunHelper: # pragma: no cover
153120
def start(self, global_config_dict_parser_config: GlobalConfigDictParserConfig) -> None:
154121
global_config_dict = get_global_config_dict(global_config_dict_parser_config=global_config_dict_parser_config)
155122

156-
# Capture head server dependencies and store in global config dict
157-
# Note: This function will modify the global config dict - update `head_server_deps`
158-
_capture_head_server_dependencies(global_config_dict)
159-
160123
# Initialize Ray cluster in the main process
161124
# Note: This function will modify the global config dict - update `ray_head_node_address`
162125
initialize_ray()
@@ -196,9 +159,7 @@ def start(self, global_config_dict_parser_config: GlobalConfigDictParserConfig)
196159

197160
dir_path = PARENT_DIR / Path(first_key, second_key)
198161

199-
head_server_deps = global_config_dict.get(HEAD_SERVER_DEPS_KEY_NAME)
200-
201-
command = f"""{_setup_env_command(dir_path, head_server_deps)} \\
162+
command = f"""{_setup_env_command(dir_path, global_config_dict)} \\
202163
&& {NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME}={escaped_config_dict_yaml_str} \\
203164
{NEMO_GYM_CONFIG_PATH_ENV_VAR_NAME}={shlex.quote(top_level_path)} \\
204165
python {str(entrypoint_fpath)}"""
@@ -423,17 +384,17 @@ def _validate_data_single(test_config: TestConfig) -> None: # pragma: no cover
423384
print(f"The data for {test_config.dir_path} has been successfully validated!")
424385

425386

426-
def _test_single(test_config: TestConfig) -> Popen: # pragma: no cover
387+
def _test_single(test_config: TestConfig, global_config_dict: DictConfig) -> Popen: # pragma: no cover
427388
# Eventually we may want more sophisticated testing here, but this is sufficient for now.
428-
command = f"""{_setup_env_command(test_config.dir_path)} && pytest"""
389+
command = f"""{_setup_env_command(test_config.dir_path, global_config_dict)} && pytest"""
429390
return _run_command(command, test_config.dir_path)
430391

431392

432393
def test(): # pragma: no cover
433-
config_dict = get_global_config_dict()
434-
test_config = TestConfig.model_validate(config_dict)
394+
global_config_dict = get_global_config_dict()
395+
test_config = TestConfig.model_validate(global_config_dict)
435396

436-
proc = _test_single(test_config)
397+
proc = _test_single(test_config, global_config_dict)
437398
return_code = proc.wait()
438399
if return_code != 0:
439400
print(f"You can run detailed tests via `cd {test_config.entrypoint} && source .venv/bin/activate && pytest`.")
@@ -482,7 +443,7 @@ def test_all(): # pragma: no cover
482443
entrypoint=str(dir_path),
483444
should_validate_data=True, # Test all always validates data.
484445
)
485-
proc = _test_single(test_config)
446+
proc = _test_single(test_config, global_config_dict)
486447
return_code = proc.wait()
487448

488449
match return_code:

nemo_gym/global_config.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
# limitations under the License.
1414
from os import getenv
1515
from pathlib import Path
16+
from platform import python_version
1617
from socket import socket
1718
from typing import ClassVar, List, Optional, Tuple, Type
1819

1920
import hydra
2021
from omegaconf import DictConfig, OmegaConf, open_dict
22+
from openai import __version__ as openai_version
2123
from pydantic import BaseModel, ConfigDict, TypeAdapter
24+
from ray import __version__ as ray_version
2225

2326
from nemo_gym import PARENT_DIR
2427
from nemo_gym.config_types import (
@@ -37,13 +40,15 @@
3740
HEAD_SERVER_KEY_NAME = "head_server"
3841
DISALLOWED_PORTS_KEY_NAME = "disallowed_ports"
3942
HEAD_SERVER_DEPS_KEY_NAME = "head_server_deps"
43+
PYTHON_VERSION_KEY_NAME = "python_version"
4044
NEMO_GYM_RESERVED_TOP_LEVEL_KEYS = [
4145
CONFIG_PATHS_KEY_NAME,
4246
ENTRYPOINT_KEY_NAME,
4347
DEFAULT_HOST_KEY_NAME,
4448
HEAD_SERVER_KEY_NAME,
4549
DISALLOWED_PORTS_KEY_NAME,
4650
HEAD_SERVER_DEPS_KEY_NAME,
51+
PYTHON_VERSION_KEY_NAME,
4752
]
4853

4954
POLICY_BASE_URL_KEY_NAME = "policy_base_url"
@@ -213,18 +218,28 @@ def parse(self, parse_config: Optional[GlobalConfigDictParserConfig] = None) ->
213218
server_instance_configs, default_host, initial_disallowed_ports
214219
)
215220

216-
# Populate head server defaults
217-
if not global_config_dict.get(HEAD_SERVER_KEY_NAME):
218-
with open_dict(global_config_dict):
221+
with open_dict(global_config_dict):
222+
# Populate head server defaults
223+
if not global_config_dict.get(HEAD_SERVER_KEY_NAME):
219224
global_config_dict[HEAD_SERVER_KEY_NAME] = {
220225
"host": default_host,
221226
"port": DEFAULT_HEAD_SERVER_PORT,
222227
}
223228

224-
# Store final list of disallowed ports.
225-
with open_dict(global_config_dict):
229+
# Store final list of disallowed ports.
226230
global_config_dict[DISALLOWED_PORTS_KEY_NAME] = disallowed_ports
227231

232+
# Constrain sensitive package versions
233+
global_config_dict[HEAD_SERVER_DEPS_KEY_NAME] = [
234+
# The ray version is very sensitive. The children ray versions must exactly match those of the parent ray.
235+
f"ray=={ray_version}",
236+
# OpenAI version is also sensitive since it changes so often and may introduce subtle incompatibilities.
237+
f"openai=={openai_version}",
238+
]
239+
240+
# Constrain python version since ray is sensitive to this.
241+
global_config_dict[PYTHON_VERSION_KEY_NAME] = python_version()
242+
228243
return global_config_dict
229244

230245
def parse_no_environment(

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ dependencies = [
7272
########################################
7373

7474
# OpenAI: We leverage OpenAI Responses, Chat Completions, and Completions schemas for Nemo Gym abstractions. It may also be used to directly query endpoints.
75-
# Updated Fri Jul 25, 2025 with openai==1.97.1
75+
# We specifically upper bound this OpenAI dependency since the version bumps so frequently.
76+
# Updated Wed Oct 29, 2025 with openai==2.6.1
7677
# License: Apache 2.0 https://github.com/openai/openai-python/blob/a8258744cbecf51321587fc870e8920bd2c07809/LICENSE
77-
"openai<=1.97.1",
78+
"openai<=2.6.1",
7879

7980
# tqdm: Use for progress tracking on batch operations.
8081
# Updated Fri Jul 25, 2025 with tqdm==4.67.1

responses_api_agents/simple_agent/tests/test_app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ async def test_responses(self, monkeypatch: MonkeyPatch) -> None:
154154
"truncation": None,
155155
"usage": None,
156156
"user": None,
157+
"conversation": None,
158+
"prompt_cache_key": None,
159+
"safety_identifier": None,
157160
}
158161
assert expected_responses_dict == actual_responses_dict
159162

@@ -324,5 +327,8 @@ async def test_responses_continues_on_reasoning_only(self, monkeypatch: MonkeyPa
324327
"truncation": None,
325328
"usage": None,
326329
"user": None,
330+
"conversation": None,
331+
"prompt_cache_key": None,
332+
"safety_identifier": None,
327333
}
328334
assert expected_responses_dict == actual_responses_dict

tests/unit_tests/test_global_config.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
from typing import Dict
1415
from unittest.mock import MagicMock
1516

1617
from pytest import MonkeyPatch, raises
@@ -30,7 +31,21 @@
3031

3132

3233
class TestServerUtils:
34+
def _mock_versions_for_testing(self, monkeypatch: MonkeyPatch) -> Dict[str, str]:
35+
monkeypatch.setattr(nemo_gym.global_config, "openai_version", "test openai version")
36+
monkeypatch.setattr(nemo_gym.global_config, "ray_version", "test ray version")
37+
38+
python_version_mock = MagicMock(return_value="test python version")
39+
monkeypatch.setattr(nemo_gym.global_config, "python_version", python_version_mock)
40+
41+
return {
42+
"head_server_deps": ["ray==test ray version", "openai==test openai version"],
43+
"python_version": "test python version",
44+
}
45+
3346
def test_get_global_config_dict_sanity(self, monkeypatch: MonkeyPatch) -> None:
47+
mock_versions_for_testing = self._mock_versions_for_testing(monkeypatch)
48+
3449
# Clear any lingering env vars.
3550
monkeypatch.delenv(NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME, raising=False)
3651
monkeypatch.setattr(nemo_gym.global_config, "_GLOBAL_CONFIG_DICT", None)
@@ -52,7 +67,11 @@ def hydra_main_wrapper(fn):
5267
monkeypatch.setattr(nemo_gym.global_config.hydra, "main", hydra_main_mock)
5368

5469
global_config_dict = get_global_config_dict()
55-
assert {"head_server": {"host": "127.0.0.1", "port": 11000}, "disallowed_ports": [11000]} == global_config_dict
70+
assert {
71+
"head_server": {"host": "127.0.0.1", "port": 11000},
72+
"disallowed_ports": [11000],
73+
**mock_versions_for_testing,
74+
} == global_config_dict
5675

5776
def test_get_global_config_dict_global_exists(self, monkeypatch: MonkeyPatch) -> None:
5877
# Clear any lingering env vars.
@@ -70,6 +89,8 @@ def test_get_global_config_dict_global_env_var(self, monkeypatch: MonkeyPatch) -
7089
assert {"a": 2} == global_config_dict
7190

7291
def test_get_global_config_dict_config_paths_sanity(self, monkeypatch: MonkeyPatch) -> None:
92+
mock_versions_for_testing = self._mock_versions_for_testing(monkeypatch)
93+
7394
# Clear any lingering env vars.
7495
monkeypatch.delenv(NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME, raising=False)
7596
monkeypatch.setattr(nemo_gym.global_config, "_GLOBAL_CONFIG_DICT", None)
@@ -103,9 +124,12 @@ def hydra_main_wrapper(fn):
103124
"extra_dot_env_key": 2,
104125
"head_server": {"host": "127.0.0.1", "port": 11000},
105126
"disallowed_ports": [11000],
127+
**mock_versions_for_testing,
106128
} == global_config_dict
107129

108130
def test_get_global_config_dict_config_paths_recursive(self, monkeypatch: MonkeyPatch) -> None:
131+
mock_versions_for_testing = self._mock_versions_for_testing(monkeypatch)
132+
109133
# Clear any lingering env vars.
110134
monkeypatch.delenv(NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME, raising=False)
111135
monkeypatch.setattr(nemo_gym.global_config, "_GLOBAL_CONFIG_DICT", None)
@@ -154,9 +178,12 @@ def omegaconf_load_mock_side_effect(path):
154178
"recursive_config_path_child_key": 3,
155179
"head_server": {"host": "127.0.0.1", "port": 11000},
156180
"disallowed_ports": [11000],
181+
**mock_versions_for_testing,
157182
} == global_config_dict
158183

159184
def test_get_global_config_dict_server_host_port_defaults(self, monkeypatch: MonkeyPatch) -> None:
185+
mock_versions_for_testing = self._mock_versions_for_testing(monkeypatch)
186+
160187
# Clear any lingering env vars.
161188
monkeypatch.delenv(NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME, raising=False)
162189
monkeypatch.setattr(nemo_gym.global_config, "_GLOBAL_CONFIG_DICT", None)
@@ -195,9 +222,12 @@ def hydra_main_wrapper(fn):
195222
"c": 2,
196223
"head_server": {"host": "127.0.0.1", "port": 11000},
197224
"disallowed_ports": [11000, 12345],
225+
**mock_versions_for_testing,
198226
} == global_config_dict
199227

200228
def test_get_global_config_dict_server_refs_sanity(self, monkeypatch: MonkeyPatch) -> None:
229+
mock_versions_for_testing = self._mock_versions_for_testing(monkeypatch)
230+
201231
# Clear any lingering env vars.
202232
monkeypatch.delenv(NEMO_GYM_CONFIG_DICT_ENV_VAR_NAME, raising=False)
203233
monkeypatch.setattr(nemo_gym.global_config, "_GLOBAL_CONFIG_DICT", None)
@@ -266,6 +296,7 @@ def hydra_main_wrapper(fn):
266296
},
267297
"head_server": {"host": "127.0.0.1", "port": 11000},
268298
"disallowed_ports": [11000, 12345, 123456],
299+
**mock_versions_for_testing,
269300
} == global_config_dict
270301

271302
def test_get_global_config_dict_server_refs_errors_on_missing(self, monkeypatch: MonkeyPatch) -> None:

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)