diff --git a/agent/relay_llm.py b/agent/relay_llm.py index 2d8c37a18f..d9454500f2 100644 --- a/agent/relay_llm.py +++ b/agent/relay_llm.py @@ -1119,6 +1119,19 @@ def _provider_request( for key, value in headers.items() if str(key).lower() not in _RELAY_INTERNAL_PROVIDER_HEADERS } + # Relay's managed-call trace header maps to ``extra_headers`` for known SDK + # adapters and custom requests that already use that container. Other + # native transports receive protocol kwargs directly and may reject a new + # SDK-only argument. Preserve non-trace middleware headers as before. + supports_extra_headers = ( + _relay_protocol(metadata) is not None or "extra_headers" in original + ) + if headers and not supports_extra_headers: + headers = { + key: value + for key, value in headers.items() + if str(key).lower() != "traceparent" + } if headers: final["extra_headers"] = { **dict(final.get("extra_headers") or {}), diff --git a/agent/relay_runtime.py b/agent/relay_runtime.py index 3dbbd397a0..405c30e8af 100644 --- a/agent/relay_runtime.py +++ b/agent/relay_runtime.py @@ -343,8 +343,8 @@ class _ProcessRelayPluginConfiguration: if self._activation is None: # Hermes only enters Relay's initialization path after an - # explicit opt-in. Relay currently owns any subsequent ambient - # layering; a future discovery=False API can make this exact. + # explicit opt-in. Relay 0.8 no longer layers repository-local + # configuration onto this explicitly selected payload. _resolve_plugin_awaitable(relay.plugin.initialize(plugin_config)) except Exception as exc: self._activation = None diff --git a/agent/relay_tools.py b/agent/relay_tools.py index 91c0926b7e..8f4aaff3f2 100644 --- a/agent/relay_tools.py +++ b/agent/relay_tools.py @@ -21,6 +21,7 @@ def execute( callback: Callable[[dict[str, Any]], Any], *, session_id: str, + tool_call_id: str | None = None, metadata: dict[str, Any] | None = None, ) -> tuple[Any, dict[str, Any]]: """Run one tool call through Relay and return its final arguments.""" @@ -52,7 +53,7 @@ def execute( raise raw_result["value"] = result raw_result["json"] = _jsonable(result) - return raw_result["json"] + return runtime.relay.ToolExecutionResult(raw_result["json"]) try: managed = _run_awaitable( @@ -64,6 +65,7 @@ def execute( invoke, handle=parent, metadata=_jsonable(metadata or {}), + tool_call_id=tool_call_id or None, ) ) except BaseException as exc: @@ -85,11 +87,12 @@ def execute( return raw_result["value"], observed_args raise - if "value" in raw_result and _json_equal(managed, raw_result["json"]): + managed_result = managed.result + if "value" in raw_result and _json_equal(managed_result, raw_result["json"]): return raw_result["value"], observed_args - if isinstance(managed, str): - return managed, observed_args - return json.dumps(_jsonable(managed), ensure_ascii=False), observed_args + if isinstance(managed_result, str): + return managed_result, observed_args + return json.dumps(_jsonable(managed_result), ensure_ascii=False), observed_args def _jsonable(value: Any) -> Any: diff --git a/agent/tool_executor.py b/agent/tool_executor.py index 5ed51b42f8..e7edd93b66 100644 --- a/agent/tool_executor.py +++ b/agent/tool_executor.py @@ -771,6 +771,7 @@ def _run_agent_tool_execution_middleware( function_args, _hermes_pipeline, session_id=str(getattr(agent, "session_id", "") or ""), + tool_call_id=tool_call_id or None, metadata={ "task_id": effective_task_id or "", "turn_id": getattr(agent, "_current_turn_id", "") or "", diff --git a/docs/observability/relay-shared-metrics.md b/docs/observability/relay-shared-metrics.md index 146590dc99..6d7850e810 100644 --- a/docs/observability/relay-shared-metrics.md +++ b/docs/observability/relay-shared-metrics.md @@ -18,18 +18,22 @@ as a no-op compatibility alias for existing installation commands. > longer activate exporters. Without the new variable, Hermes does not run > Relay plugin discovery, configuration layering, middleware, or exporters. -Hermes requires NeMo Relay 0.7.1 or later within the 0.7 release line. That -release establishes the lossless provider-codec contract used for Anthropic -Messages, OpenAI Chat Completions, and OpenAI Responses requests. +Hermes requires NeMo Relay 0.8.1 or later within the 0.8 release line. That +line provides the provider-codec and canonical tool-result contracts Hermes +uses for managed provider and tool calls. ## Runtime Dependency and Data Boundary Hermes installs the platform-specific `nemo-relay` native wheel from the -bounded `>=0.7.1,<0.8` dependency range. The published package is built from +bounded `>=0.8.1,<0.9` dependency range. The published package is built from the [NVIDIA NeMo Relay repository](https://github.com/NVIDIA/NeMo-Relay). Unsupported platforms use the explicit no-op runtime described above rather than downloading a different implementation. +Operator-supplied typed native plugins must be rebuilt for Relay 0.8. `grpc-v1` +workers must be regenerated and rebuilt when they use tool callbacks, tool +execution intercepts, or manual tool-end APIs. + When Relay managed execution is active, the provider request and response pass through that native module in the Hermes process so configured interceptors can operate on the real call. This is separate from the shared-metrics data @@ -55,12 +59,12 @@ opt-in. Set `HERMES_NEMO_RELAY_PLUGINS_TOML` to a selected `plugins.toml` to activate configured middleware, exporters, or dynamic plugins. When the variable is unset, Hermes does not invoke Relay's plugin initializer, so Relay does not perform plugin configuration discovery or layering. When it is set -and the selected file loads successfully, Relay performs its normal static -`plugins.toml` discovery and layers the selected static configuration over the -discovered configuration. Dynamic `[[plugins.dynamic]]` records are loaded -from the selected file only. If the selected file cannot be loaded, Hermes -reports the error and does not invoke Relay initialization or fall back to -ambient discovery. +and the selected file loads successfully, Relay discovers supported user and +system `plugins.toml` files and layers the selected static configuration over +them. Repository-local `.nemo-relay/plugins.toml` files are ignored. Dynamic +`[[plugins.dynamic]]` records are loaded from the selected file only. If the +selected file cannot be loaded, Hermes reports the error and does not invoke +Relay initialization or fall back to ambient discovery. ## Session-Span Segmentation for Continuous Sessions diff --git a/hermes_cli/observability/relay_shared_metrics.py b/hermes_cli/observability/relay_shared_metrics.py index 2ab88f51c3..9dde5f763f 100644 --- a/hermes_cli/observability/relay_shared_metrics.py +++ b/hermes_cli/observability/relay_shared_metrics.py @@ -906,7 +906,7 @@ class _Runtime: task, self.relay.tools.call_end, tool_call.handle, - fields, + self.relay.ToolExecutionResult(fields), metadata=self._event_metadata(), ) except Exception: diff --git a/pyproject.toml b/pyproject.toml index d1f213e492..ae75800863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,13 +152,13 @@ dependencies = [ # Hence the ``sys_platform == 'win32'`` marker: the dep (and its portalocker # / pywin32 tree) ships only where it's actually used. "concurrent-log-handler==0.9.29; sys_platform == 'win32'", - # First-party lifecycle and shared-metrics runtime. Relay 0.6 is the minimum - # lossless provider-codec contract. Managed calls pass request/response data - # through this native module in-process; shared metrics installs no network - # exporter and consumes only its bounded projection. Relay publishes wheels - # only (no sdist), so this marker must stay false anywhere no wheel tag can - # match — otherwise installing Python dependencies fails resolution outright - # instead of falling back to the no-op Relay host (#76469, Termux). + # First-party lifecycle and shared-metrics runtime. Relay 0.8 is the supported + # native runtime and provider-codec baseline. Managed calls pass request and + # response data through this native module in-process; shared metrics installs + # no network exporter and consumes only its bounded projection. This marker + # must stay false anywhere no compatible native wheel tag can match; otherwise + # installing Python dependencies fails instead of falling back to the no-op + # Relay host (#76469, Termux). # Termux Python reports plain linux/aarch64 but runs on Bionic # libc, which satisfies neither manylinux nor musllinux, hence the # `'android' not in platform_release` guard on the linux arms: Android GKI @@ -166,7 +166,7 @@ dependencies = [ # 738 CPython reports sys_platform == 'android' and never matched.) Pre-GKI # devices can still slip through; they get the same resolution failure as # before, worked around by installing with `--no-deps` or an older release. - "nemo-relay>=0.7.1,<0.8; (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'linux' and platform_machine == 'x86_64' and 'android' not in platform_release) or (sys_platform == 'linux' and platform_machine == 'aarch64' and 'android' not in platform_release) or (sys_platform == 'win32' and platform_machine == 'AMD64') or (sys_platform == 'win32' and platform_machine == 'ARM64')", + "nemo-relay>=0.8.1,<0.9; (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'linux' and platform_machine == 'x86_64' and 'android' not in platform_release) or (sys_platform == 'linux' and platform_machine == 'aarch64' and 'android' not in platform_release) or (sys_platform == 'win32' and platform_machine == 'AMD64') or (sys_platform == 'win32' and platform_machine == 'ARM64')", ] [project.optional-dependencies] diff --git a/tests/agent/test_relay_llm.py b/tests/agent/test_relay_llm.py index f091fb1222..a31553af88 100644 --- a/tests/agent/test_relay_llm.py +++ b/tests/agent/test_relay_llm.py @@ -259,6 +259,141 @@ def test_provider_request_overlays_interceptor_added_extra_body(): assert provider_request["extra_body"] == {"prompt_cache_retention": "24h"} +@pytest.mark.parametrize( + "api_mode", + ["chat_completions", "codex_responses", "anthropic_messages"], +) +def test_provider_request_maps_headers_for_supported_sdk_modes(api_mode): + original = {"model": "test-model"} + relay_request_body = relay_llm._relay_request_body( + original, + {"api_mode": api_mode}, + ) + + provider_request = relay_llm._provider_request( + original, + SimpleNamespace( + content=relay_request_body, + headers={ + "traceparent": ( + "00-11111111111111111111111111111111-" + "2222222222222222-01" + ) + }, + ), + relay_request_body=relay_request_body, + codec_baseline_body=dict(relay_request_body), + metadata={"api_mode": api_mode}, + ) + + assert provider_request["extra_headers"] == { + "traceparent": ( + "00-11111111111111111111111111111111-2222222222222222-01" + ) + } + + +def test_provider_request_preserves_custom_headers_for_native_transport(): + original = {"payload": "provider-native"} + + provider_request = relay_llm._provider_request( + original, + SimpleNamespace( + content=original, + headers={ + "traceparent": ( + "00-11111111111111111111111111111111-" + "2222222222222222-01" + ), + "x-custom-route": "private", + }, + ), + relay_request_body=original, + codec_baseline_body=dict(original), + metadata={"api_mode": "strict_native"}, + ) + + assert provider_request["extra_headers"] == { + "x-custom-route": "private" + } + + +def test_provider_request_traces_custom_transport_with_header_capability(): + original = { + "payload": "provider-native", + "extra_headers": {"authorization": "Bearer provider-token"}, + } + traceparent = ( + "00-11111111111111111111111111111111-2222222222222222-01" + ) + + provider_request = relay_llm._provider_request( + original, + SimpleNamespace( + content=original, + headers={"traceparent": traceparent}, + ), + relay_request_body=original, + codec_baseline_body=dict(original), + metadata={"api_mode": "custom"}, + ) + + assert provider_request["extra_headers"] == { + "authorization": "Bearer provider-token", + "traceparent": traceparent, + } + + +def test_managed_request_does_not_add_sdk_headers_to_strict_callback(relay_turn): + del relay_turn + observed = [] + + def strict_transport(*, payload): + observed.append(payload) + return {"content": payload} + + result = relay_llm.execute( + {"payload": "provider-native"}, + lambda request: strict_transport(**request), + session_id="session-1", + name="strict-native", + model_name="strict-model", + metadata={ + "api_mode": "bedrock_converse", + "api_request_id": "strict-native-request", + }, + ) + + assert observed == ["provider-native"] + assert result == {"content": "provider-native"} + + +def test_managed_stream_does_not_add_sdk_headers_to_strict_callback(relay_turn): + del relay_turn + observed = [] + chunks = [{"delta": "provider-native"}] + + def strict_transport(*, payload): + observed.append(payload) + return iter(chunks) + + stream = relay_llm.stream( + {"payload": "provider-native"}, + lambda request: strict_transport(**request), + session_id="session-1", + name="strict-native", + model_name="strict-model", + finalizer=lambda: {"content": "provider-native"}, + metadata={ + "api_mode": "bedrock_converse", + "api_request_id": "strict-native-stream", + }, + ) + + assert list(stream) == chunks + assert observed == ["provider-native"] + + def test_stream_uses_rewritten_request_and_post_intercept_chunks(relay_turn): relay, turn = relay_turn captured_requests = [] @@ -355,9 +490,15 @@ def test_stream_uses_rewritten_request_and_post_intercept_chunks(relay_turn): relay.intercepts.deregister_llm_request("hermes-test-request") assert captured_requests[0]["temperature"] == 0.25 - assert captured_requests[0]["extra_headers"] == { - "authorization": "Bearer provider-token" - } + headers = captured_requests[0]["extra_headers"] + assert headers["authorization"] == "Bearer provider-token" + version, trace_id, parent_id, flags = headers["traceparent"].split("-") + assert version == "00" + assert len(trace_id) == 32 + assert len(parent_id) == 16 + assert flags == "01" + int(trace_id, 16) + int(parent_id, 16) assert chunks[0].choices[0].delta.content == "HELLO" assert stream.output_modified is True assert turn.logical_llm_calls == {} diff --git a/tests/agent/test_relay_runtime_plugins.py b/tests/agent/test_relay_runtime_plugins.py index a906f94f29..214d80d24d 100644 --- a/tests/agent/test_relay_runtime_plugins.py +++ b/tests/agent/test_relay_runtime_plugins.py @@ -1049,7 +1049,7 @@ mode = "overwrite" assert not (atof_dir / "events.jsonl").exists() -def test_real_binding_layers_project_config_after_explicit_opt_in( +def test_real_binding_ignores_project_config_with_explicit_opt_in( tmp_path, monkeypatch, ): @@ -1061,7 +1061,8 @@ def test_real_binding_layers_project_config_after_explicit_opt_in( working_directory = project_root / "workspace" config_directory = project_root / ".nemo-relay" selected_directory = tmp_path / "selected-config" - atof_dir = tmp_path / "atof" + project_atof_dir = tmp_path / "project-atof" + selected_atof_dir = tmp_path / "selected-atof" working_directory.mkdir(parents=True) config_directory.mkdir() selected_directory.mkdir() @@ -1081,14 +1082,35 @@ enabled = true [[components.config.atof.sinks]] type = "file" -output_directory = "{atof_dir}" +output_directory = "{project_atof_dir}" filename = "events.jsonl" mode = "overwrite" """.strip(), encoding="utf-8", ) selected_config = selected_directory / "plugins.toml" - selected_config.write_text("version = 1", encoding="utf-8") + selected_config.write_text( + f""" +version = 1 + +[[components]] +kind = "observability" +enabled = true + +[components.config] +version = 4 + +[components.config.atof] +enabled = true + +[[components.config.atof.sinks]] +type = "file" +output_directory = "{selected_atof_dir}" +filename = "events.jsonl" +mode = "overwrite" +""".strip(), + encoding="utf-8", + ) xdg_config_home = tmp_path / "xdg" xdg_config_home.mkdir() monkeypatch.chdir(working_directory) @@ -1102,12 +1124,13 @@ mode = "overwrite" host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile") try: assert host.managed_execution_enabled() - host.ensure_session({"session_id": "native-layered-plugins"}) + host.ensure_session({"session_id": "native-explicit-plugins"}) finally: host.shutdown() relay_runtime._reset_for_tests() - assert (atof_dir / "events.jsonl").is_file() + assert (selected_atof_dir / "events.jsonl").is_file() + assert not (project_atof_dir / "events.jsonl").exists() def test_real_binding_keeps_two_profile_trajectories_separate_in_shared_exporters( diff --git a/tests/agent/test_relay_tools.py b/tests/agent/test_relay_tools.py index 22dec2fe52..760aa7335b 100644 --- a/tests/agent/test_relay_tools.py +++ b/tests/agent/test_relay_tools.py @@ -78,7 +78,10 @@ def test_request_rewrite_reaches_authorized_callback_once(relay_turn): async def wrap_execution(_name, args, next_call): result = await next_call(args) - return relay.ToolExecutionInterceptOutcome({**result, "wrapped": True}) + return relay.ToolExecutionInterceptOutcome( + {**result.result, "wrapped": True}, + annotation={"audit": "annotation-canary"}, + ) relay.intercepts.register_tool_request( "hermes-test-tool-request", 1, False, rewrite_request @@ -102,10 +105,33 @@ def test_request_rewrite_reaches_authorized_callback_once(relay_turn): assert observed_args == {"path": "/approved/path"} assert isinstance(result, str) assert json.loads(result) == {"ok": True, "wrapped": True} + assert "annotation-canary" not in result +def test_tool_call_id_uses_canonical_relay_argument(relay_turn, monkeypatch): + relay = relay_turn + captured = {} + async def capture_execute(_name, args, callback, **kwargs): + captured.update(kwargs) + result = callback(args) + assert isinstance(result, relay.ToolExecutionResult) + return result + monkeypatch.setattr(relay.tools, "execute", capture_execute) + original_result = {"ok": True} + + result, observed_args = relay_tools.execute( + "write_file", + {"path": "/tmp/output"}, + lambda _args: original_result, + session_id="session-1", + tool_call_id="call-42", + ) + + assert result is original_result + assert observed_args == {"path": "/tmp/output"} + assert captured["tool_call_id"] == "call-42" def test_tool_error_is_preserved_from_relay_wrapper_suffix(relay_turn, monkeypatch): @@ -135,8 +161,3 @@ def test_tool_error_is_preserved_from_relay_wrapper_suffix(relay_turn, monkeypat ) assert caught.value is tool_error - - - - - diff --git a/tests/hermes_cli/test_relay_shared_metrics_runtime.py b/tests/hermes_cli/test_relay_shared_metrics_runtime.py index 998fae07b2..e7ada02063 100644 --- a/tests/hermes_cli/test_relay_shared_metrics_runtime.py +++ b/tests/hermes_cli/test_relay_shared_metrics_runtime.py @@ -25,6 +25,12 @@ class _Request: self.content = content +class _ToolExecutionResult: + def __init__(self, result: Any, annotation: Any = None) -> None: + self.result = result + self.annotation = annotation + + class _Relay: def __init__(self) -> None: self.events: list[tuple[Any, ...]] = [] @@ -39,6 +45,7 @@ class _Relay: Agent="agent", Function="function", Tool="tool" ) self.LLMRequest = _Request + self.ToolExecutionResult = _ToolExecutionResult self.scope = SimpleNamespace( push=self._scope_push, pop=self._scope_pop, @@ -174,11 +181,13 @@ class _Relay: def _tool_call_end( self, handle: Any, - result: dict[str, Any], + result: _ToolExecutionResult, **kwargs: Any, ) -> None: + assert isinstance(result, _ToolExecutionResult) + payload = result.result start = self._tool_starts.pop(handle) - self.events.append(("tool.call_end", handle, result, kwargs)) + self.events.append(("tool.call_end", handle, payload, kwargs)) event = SimpleNamespace( kind="scope", category="tool", @@ -190,7 +199,7 @@ class _Relay: **kwargs["metadata"], "otel.status_code": "OK", }, - data=result, + data=payload, ) for callback in list(self._callbacks.values()): callback(event) diff --git a/uv.lock b/uv.lock index 3f7b7946fb..f054dd40cd 100644 --- a/uv.lock +++ b/uv.lock @@ -820,7 +820,7 @@ name = "ctranslate2" version = "4.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, { name = "pyyaml" }, { name = "setuptools" }, ] @@ -1887,7 +1887,7 @@ requires-dist = [ { name = "microsoft-teams-apps", marker = "extra == 'teams'", specifier = "==2.0.13.4" }, { name = "mistralai", marker = "extra == 'mistral'", specifier = "==2.4.8" }, { name = "modal", marker = "extra == 'modal'", specifier = "==1.3.4" }, - { name = "nemo-relay", marker = "(platform_machine == 'aarch64' and 'android' not in platform_release and sys_platform == 'linux') or (platform_machine == 'x86_64' and 'android' not in platform_release and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'ARM64' and sys_platform == 'win32')", specifier = ">=0.7.1,<0.8" }, + { name = "nemo-relay", marker = "(platform_machine == 'aarch64' and 'android' not in platform_release and sys_platform == 'linux') or (platform_machine == 'x86_64' and 'android' not in platform_release and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'ARM64' and sys_platform == 'win32')", specifier = ">=0.8.1,<0.9" }, { name = "numpy", marker = "extra == 'voice'", specifier = "==2.4.3" }, { name = "numpy", marker = "extra == 'wake'", specifier = "==2.4.3" }, { name = "onnxruntime", marker = "extra == 'wake'", specifier = "==1.27.0" }, @@ -2751,17 +2751,17 @@ wheels = [ [[package]] name = "nemo-relay" -version = "0.7.2" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/81/a7a545ac3a2f8c670d261c89df599aa8fbf49d8be45fd1f52efb36b489eb/nemo_relay-0.7.2.tar.gz", hash = "sha256:828d9f6c7d7e4e42276bb7192bd44202c761e0c76fa4943d84e051b5a99028e5", size = 1295616, upload-time = "2026-08-08T01:54:00.953Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ad/87a2008ef4dc8993ef7d10058eb6b0bd054f541be85e695b047e34b2fb70/nemo_relay-0.8.1.tar.gz", hash = "sha256:37d2ba9b8a4067da0559b77c3d32167a8ef5994705dd4c1414cadaf434812636", size = 1613522, upload-time = "2026-08-28T03:24:07.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cd/f50440257f01bc5ab3d668331c90e06cf4edcc84dc7dc582d322ad05b622/nemo_relay-0.7.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:e7c7977f0903793cc34c5542bf2b2e44d107def8a5ae9f1b28f06dd61ddec4ed", size = 9246341, upload-time = "2026-08-08T01:53:19.832Z" }, - { url = "https://files.pythonhosted.org/packages/ed/9f/4041446dd134218799a34b5b5fad3a62d3e1d0a6c322ba2ca4b896ba1393/nemo_relay-0.7.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ae77c1f3d58eabda264e82ffaca54548df80caede7dd6af8cbd8f72b4a82ed", size = 8454070, upload-time = "2026-08-08T01:53:22.524Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/90230c2e9fae1aee39f768d4a9ef57e9f2716bcaed1a5923cce8b526c66b/nemo_relay-0.7.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ce7103aec546766649c182619d16aa6ad07439e4d0ebd16d95c5004afb3e56a", size = 8954377, upload-time = "2026-08-08T01:53:25.267Z" }, - { url = "https://files.pythonhosted.org/packages/71/e7/463fa461d0801146fec6a00cbc02e8961b30089d65ba170f9dfa9e6e3dcd/nemo_relay-0.7.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e7d0c2629ade7313aaed71d2272dca96a2fafad0248d0f87cf40a7720b252a0", size = 10322132, upload-time = "2026-08-08T01:53:27.991Z" }, - { url = "https://files.pythonhosted.org/packages/32/8c/e20ec9c52bd1edd953157aaf24d0d9f9ab8afcbf108fc2356f398e252da8/nemo_relay-0.7.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b841c92395d7686c7f233036008294b9d362af1ec5123ab0babbfd11cbb04054", size = 10704141, upload-time = "2026-08-08T01:53:30.453Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c1/92a73961ea759b433b1f897b225662d499123cb962b48dc8ece19f610a09/nemo_relay-0.7.2-cp311-abi3-win_amd64.whl", hash = "sha256:0cdcc5e09d6d62d5c1d385dc62c9233eb714a25f36a09da81e5b9731e3c67903", size = 8803938, upload-time = "2026-08-08T01:53:33.437Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ec/2de114dab437431173988b9b11f46e8d377e12d57e1b4903258f3e03c2df/nemo_relay-0.7.2-cp311-abi3-win_arm64.whl", hash = "sha256:ca5f66e617311f836a10d96f120f3f32a99b4267d65048453b31951de3419a9d", size = 8438997, upload-time = "2026-08-08T01:53:36.12Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/6202e861e0d28d0f8fcc6f569d7f8b46a0b23559bf748ab254357557516d/nemo_relay-0.8.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:6ebcaa9c9ed60faab4b77d597aa7cfb8bb08f47736de1daef38011858716cbfd", size = 10197028, upload-time = "2026-08-28T03:23:23.241Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f4/130b000a0ca1dec9f7081773fd9580033d4bffcf8cf31b0520a55a4b305b/nemo_relay-0.8.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a127bd50a3579ad1e979274795423c2c825728b3ed2fa4c77fafb86ca5db68", size = 9196752, upload-time = "2026-08-28T03:23:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9d/aac5509a398d6db0b03d26871c3921cd320373b82c8a2a254556ecbb31ba/nemo_relay-0.8.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac29e367a5b738fcaaf7035241dd400dd71f8db9c24872b018fb0036d8ba00f", size = 9761648, upload-time = "2026-08-28T03:23:29.372Z" }, + { url = "https://files.pythonhosted.org/packages/4d/93/6ba04df47d9853938c8e1577a90ee4acbec129116e64ac28d3c2df0a4b50/nemo_relay-0.8.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f83bd545eee75a62b292ae8d36958f2ef6ef5a438818628933f2f9b465d5ba0f", size = 11288923, upload-time = "2026-08-28T03:23:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/8df4f1e66f9c2d6300085ee45f85917b0c030f18155fcd329d2653684437/nemo_relay-0.8.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1929fb000433dcc083de12f40179e2868fbe11561fd3d4d13ad494c841b4d69f", size = 11718787, upload-time = "2026-08-28T03:23:35.244Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d8/47cd78598c90a8127192ad2fb95c6a77450fabd4cf4dedd0aeba7c2888ba/nemo_relay-0.8.1-cp311-abi3-win_amd64.whl", hash = "sha256:8f7ea57a2ae1a278a9667c4d015dee5dca09fe0dd7fae9246db531fd75c614e3", size = 9657290, upload-time = "2026-08-28T03:23:38.496Z" }, + { url = "https://files.pythonhosted.org/packages/fd/72/3caf3258b4daa2b2a09603a2d0ec6999fbb12359fb5306ade715665bb9c8/nemo_relay-0.8.1-cp311-abi3-win_arm64.whl", hash = "sha256:ae06fd0cdc5149091441f9e3fbb4ea41a4febaad858676c2289552322442afbe", size = 9233571, upload-time = "2026-08-28T03:23:41.516Z" }, ] [[package]] @@ -4052,7 +4052,7 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4107,7 +4107,7 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [