from types import SimpleNamespace import pytest pytest.importorskip("nemo_relay") from agent import auxiliary_client, relay_llm, relay_runtime from hermes_cli.observability.shared_metrics import SharedMetricsStore from hermes_cli.observability.shared_metrics_contract import MODEL_ROUTE_METRIC from hermes_cli.observability.shared_metrics_subscriber import SharedMetricsSubscriber @pytest.fixture() def relay_turn(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path / "profile")) relay_runtime._reset_for_tests() lease = relay_runtime.SESSION_COORDINATOR.acquire_conversation( profile_key=relay_runtime.current_profile_key(), session_id="session-1", platform="cli", ) turn = relay_runtime.SESSION_COORDINATOR.begin_turn( lease, turn_id="turn-1", task_id="task-1", ) try: yield lease.host.relay, turn finally: relay_runtime.SESSION_COORDINATOR.end_turn(turn, outcome="success") relay_runtime.SESSION_COORDINATOR.release_conversation(lease) relay_runtime._reset_for_tests() def test_auxiliary_retries_share_logical_relay_identity(monkeypatch): attempts = [] logical_completions = [] responses = iter([ SimpleNamespace(choices=[]), SimpleNamespace( choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))] ), ]) client = SimpleNamespace( chat=SimpleNamespace( completions=SimpleNamespace( create=lambda **_kwargs: next(responses), ) ) ) def execute_current(request, callback, **kwargs): attempts.append(kwargs) return callback(request) monkeypatch.setattr(relay_llm, "execute_current", execute_current) monkeypatch.setattr( relay_llm, "complete_logical_call", lambda request_id, *, outcome, model_name, provider_name, response_model_name: logical_completions.append( (request_id, outcome, model_name, provider_name, response_model_name) ), ) @auxiliary_client._relay_auxiliary_call def run(task): auxiliary_client._set_relay_auxiliary_route( "openrouter", "test-model", "chat_completions", ) with pytest.raises(RuntimeError, match="invalid response"): auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( client, {"model": "test-model", "messages": []}, ), task, ) return auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( client, {"model": "test-model", "messages": []}, ), task, ) result = run("compression") assert result.choices[0].message.content == "ok" assert attempts[0]["metadata"]["api_request_id"] == ( attempts[1]["metadata"]["api_request_id"] ) assert [attempt["metadata"]["retry_count"] for attempt in attempts] == [0, 1] assert attempts[0]["metadata"]["call_role"] == "auxiliary:compression" assert all(attempt["defer_logical_completion"] is True for attempt in attempts) assert logical_completions == [ ( attempts[0]["metadata"]["api_request_id"], "success", "test-model", "openrouter", None, ) ] def test_auxiliary_provider_fallback_closes_one_real_logical_call( relay_turn, monkeypatch, ): relay, turn = relay_turn consumer = "test.auxiliary-provider-fallback" turn.lease.host.retain_managed_execution(consumer) logical_outputs = [] original_pop = relay.scope.pop def record_pop(*args, **kwargs): logical_outputs.append(kwargs.get("output") or {}) return original_pop(*args, **kwargs) monkeypatch.setattr(relay.scope, "pop", record_pop) responses = iter([ SimpleNamespace(choices=[]), SimpleNamespace( choices=[SimpleNamespace(message=SimpleNamespace(content="recovered"))] ), ]) client = SimpleNamespace( chat=SimpleNamespace( completions=SimpleNamespace( create=lambda **_kwargs: next(responses), ) ) ) @auxiliary_client._relay_auxiliary_call def run(task): auxiliary_client._set_relay_auxiliary_route( "nvidia", "nvidia/test-model", "chat_completions", ) with pytest.raises(RuntimeError, match="invalid response"): auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( client, {"model": "nvidia/test-model", "messages": []}, ), task, ) assert len(turn.logical_llm_calls) == 1 auxiliary_client._set_relay_auxiliary_route( "openrouter", "openrouter/test-model", "chat_completions", ) return auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( client, {"model": "openrouter/test-model", "messages": []}, ), task, ) try: result = run("compression") finally: turn.lease.host.release_managed_execution(consumer) assert result.choices[0].message.content == "recovered" assert turn.logical_llm_calls == {} assert logical_outputs == [ { "model": "openrouter/test-model", "outcome": "success", "provider": "openrouter", } ] def test_auxiliary_provider_fallback_records_one_terminal_model_route( relay_turn, tmp_path, ): relay, turn = relay_turn store = SharedMetricsStore( tmp_path / "metrics.sqlite3", tmp_path / "outbox", ) subscriber = SharedMetricsSubscriber( store, "test-version", runtime_id=turn.lease.host.runtime_id, ) subscriber_name = "test.auxiliary-model-route" relay.subscribers.register(subscriber_name, subscriber) turn.lease.host.retain_managed_execution(subscriber_name) responses = iter([ SimpleNamespace(model="failed/model", choices=[]), SimpleNamespace( model="Accepted/Model", choices=[SimpleNamespace(message=SimpleNamespace(content="recovered"))], ), ]) client = SimpleNamespace( chat=SimpleNamespace( completions=SimpleNamespace( create=lambda **_kwargs: next(responses), ) ) ) @auxiliary_client._relay_auxiliary_call def run(task): auxiliary_client._set_relay_auxiliary_route( "nvidia", "failed/configured-model", "chat_completions", ) with pytest.raises(RuntimeError, match="invalid response"): auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( client, {"model": "failed/configured-model", "messages": []}, ), task, ) auxiliary_client._set_relay_auxiliary_route( "OpenRouter", "fallback/configured-model", "chat_completions", ) return auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( client, {"model": "fallback/configured-model", "messages": []}, ), task, ) try: result = run("compression") relay.subscribers.flush() finally: turn.lease.host.release_managed_execution(subscriber_name) relay.subscribers.deregister(subscriber_name) assert result.choices[0].message.content == "recovered" snapshot = store.counter_snapshot() assert len(snapshot) == 1 assert snapshot[0]["metric_name"] == MODEL_ROUTE_METRIC assert snapshot[0]["resource"]["hermes_version"] == "test-version" assert snapshot[0]["dimensions"] == { "model": "accepted/model", "provider": "openrouter", } assert snapshot[0]["value"] == 1 assert snapshot[0]["packaged_value"] == 0 @pytest.mark.asyncio async def test_async_auxiliary_attempt_uses_inherited_relay_adapter(monkeypatch): captured = {} logical_completions = [] async def create(**kwargs): return SimpleNamespace( request=kwargs, choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))], ) client = SimpleNamespace( chat=SimpleNamespace(completions=SimpleNamespace(create=create)) ) async def execute_current_async(request, callback, **kwargs): captured.update(kwargs) return await callback(request) monkeypatch.setattr( relay_llm, "execute_current_async", execute_current_async, ) monkeypatch.setattr( relay_llm, "complete_logical_call", lambda request_id, *, outcome, model_name, provider_name, response_model_name: logical_completions.append( (request_id, outcome, model_name, provider_name, response_model_name) ), ) @auxiliary_client._relay_auxiliary_call_async async def run(task): auxiliary_client._set_relay_auxiliary_route( "anthropic", "claude-test", "chat_completions", ) return auxiliary_client._validate_llm_response( await auxiliary_client._relay_async_completion( client, {"model": "claude-test", "messages": []}, ), task, ) result = await run("title_generation") assert result.request["model"] == "claude-test" assert captured["name"] == "anthropic" assert captured["metadata"]["call_role"] == "auxiliary:title_generation" assert captured["defer_logical_completion"] is True assert logical_completions == [ ( captured["metadata"]["api_request_id"], "success", "claude-test", "anthropic", None, ) ] def test_partial_auxiliary_stream_failure_closes_before_recovery( relay_turn, monkeypatch ): _relay, turn = relay_turn consumer = "test.partial-auxiliary-stream-failure" turn.lease.host.retain_managed_execution(consumer) logical_outputs = [] original_pop = turn.lease.host.relay.scope.pop def record_pop(*args, **kwargs): logical_outputs.append(kwargs.get("output") or {}) return original_pop(*args, **kwargs) monkeypatch.setattr(turn.lease.host.relay.scope, "pop", record_pop) class ProviderError(Exception): pass provider_error = ProviderError("stream failed") partial_chunk = SimpleNamespace( model="test-model", choices=[ SimpleNamespace( delta=SimpleNamespace(content="partial", tool_calls=None), finish_reason=None, ) ], usage=None, ) def partial_stream(): yield partial_chunk raise provider_error stream_client = SimpleNamespace( chat=SimpleNamespace( completions=SimpleNamespace( create=lambda **_kwargs: partial_stream(), ) ) ) recovery_client = SimpleNamespace( chat=SimpleNamespace( completions=SimpleNamespace( create=lambda **_kwargs: SimpleNamespace( choices=[ SimpleNamespace(message=SimpleNamespace(content="recovered")) ] ), ) ) ) @auxiliary_client._relay_auxiliary_call def start_stream(task): auxiliary_client._set_relay_auxiliary_route( "openrouter", "test-model", "chat_completions", ) return auxiliary_client._relay_sync_stream( stream_client, {"model": "test-model", "messages": [], "stream": True}, ) @auxiliary_client._relay_auxiliary_call def recover(task): auxiliary_client._set_relay_auxiliary_route( "openrouter", "test-model", "chat_completions", ) return auxiliary_client._validate_llm_response( auxiliary_client._relay_sync_completion( recovery_client, {"model": "test-model", "messages": []}, ), task, ) try: stream = start_stream("moa") assert next(stream) is partial_chunk with pytest.raises(ProviderError) as caught: next(stream) assert caught.value is provider_error assert logical_outputs == [ { "model": "test-model", "outcome": "failed", "provider": "openrouter", } ] assert turn.logical_llm_calls == {} result = recover("moa") assert result.choices[0].message.content == "recovered" assert logical_outputs == [ { "model": "test-model", "outcome": "failed", "provider": "openrouter", }, { "model": "test-model", "outcome": "success", "provider": "openrouter", }, ] assert turn.logical_llm_calls == {} finally: turn.lease.host.release_managed_execution(consumer) def test_auxiliary_stream_unwraps_completed_response(relay_turn): """MoA aggregator on an Anthropic-protocol provider: the client returns a completed response for ``stream=True`` (the adapter ignores the flag), so ``_relay_sync_stream`` must surface it raw for the consumer's ``hasattr(stream, "choices")`` handling — regression of #11732/#55933 via the Relay integration (SimpleNamespace is not iterable).""" _relay, _turn = relay_turn completed = SimpleNamespace( choices=[ SimpleNamespace( message=SimpleNamespace(content="aggregated"), finish_reason="stop", ) ], model="kimi-k3", ) client = SimpleNamespace( chat=SimpleNamespace( completions=SimpleNamespace(create=lambda **_kwargs: completed) ) ) @auxiliary_client._relay_auxiliary_call def run(task): auxiliary_client._set_relay_auxiliary_route( "kimi-coding", "kimi-k3", "chat_completions", ) return auxiliary_client._relay_sync_stream( client, {"model": "kimi-k3", "messages": [], "stream": True}, ) assert run("moa_aggregator") is completed def test_call_llm_stream_unwraps_completed_response(relay_turn, monkeypatch): """Outermost seam: ``call_llm(stream=True)`` — decorated with ``@_relay_auxiliary_call`` in production, so the Relay context is always set — with an Anthropic-shaped client that ignores ``stream=True`` and returns a completed response (the MoA aggregator on kimi-coding / MiniMax / ZAI / any /anthropic gateway). Must return the raw response for the consumer's ``hasattr(stream, "choices")`` handling, not crash with ``TypeError: 'types.SimpleNamespace' object is not iterable``.""" _relay, _turn = relay_turn captured = {} completed = SimpleNamespace( choices=[ SimpleNamespace( message=SimpleNamespace(content="aggregated"), finish_reason="stop", ) ], model="kimi-k3", ) def fake_create(**kwargs): captured.update(kwargs) return completed client = SimpleNamespace( base_url="https://api.kimi.com/coding/v1", chat=SimpleNamespace(completions=SimpleNamespace(create=fake_create)), ) monkeypatch.setattr( auxiliary_client, "_get_cached_client", lambda *args, **kwargs: (client, "kimi-k3"), ) result = auxiliary_client.call_llm( "moa_aggregator", provider="kimi-coding", model="kimi-k3", api_key="sk-test", messages=[{"role": "user", "content": "q"}], stream=True, stream_options={"include_usage": True}, ) assert result is completed assert captured["stream"] is True assert captured["stream_options"] == {"include_usage": True}