From 1059f68bcaacb7820ed0b637d42f8f82bc7c806f Mon Sep 17 00:00:00 2001 From: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:07:37 +0530 Subject: [PATCH] fix(gateway): preserve runtime provider model in agent handoff (#48061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_resolve_runtime_agent_kwargs()` did not surface the runtime provider's explicit `model`, and the one caller that handled it did so inline. Other agent construction sites (`api_server._create_agent`, the Feishu comment path) built the agent from `**runtime_kwargs` without consuming a runtime `model`, so when the runtime provider supplied an explicit model it either collided with the separate `model=` constructor arg or was dropped entirely — the gateway sent an empty/wrong runtime model (`MODEL:'' PROVIDER:None`). Surface `model` in `_resolve_runtime_agent_kwargs()` and extract the apply-and-pop logic into a shared `_consume_runtime_model(model, runtime_kwargs)` helper. Apply it at every agent-construction site that forwards `**runtime_kwargs`: - `GatewayRunner` (refactored from the existing inline consume) - `api_server._create_agent` (the #48061 root path) - `feishu_comment._resolve_model_and_runtime` (sibling call site) This closes the whole bug class — `model` is consumed as the explicit `model=` arg at each site instead of leaking through `**runtime_kwargs`. Salvaged from #49899 by Tranquil-Flow (authorship preserved). Tests: tests/gateway/test_runtime_provider_model_handoff.py (4) — the runtime model is applied and popped so it can't collide; tests/gateway/test_api_server.py (167) green. Fixes #48061 --- gateway/platforms/api_server.py | 2 + gateway/run.py | 33 ++++-- plugins/platforms/feishu/feishu_comment.py | 3 +- tests/gateway/test_api_server.py | 35 ++++++ .../test_runtime_provider_model_handoff.py | 102 ++++++++++++++++++ 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 tests/gateway/test_runtime_provider_model_handoff.py diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 013bce5717f..fa81d5e3875 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -1087,6 +1087,7 @@ class APIServerAdapter(BasePlatformAdapter): """ from run_agent import AIAgent from gateway.run import ( + _consume_runtime_model, _current_max_iterations, _resolve_runtime_agent_kwargs, _resolve_gateway_model, @@ -1098,6 +1099,7 @@ class APIServerAdapter(BasePlatformAdapter): runtime_kwargs = _resolve_runtime_agent_kwargs() reasoning_config = GatewayRunner._load_reasoning_config() model = _resolve_gateway_model() + model, runtime_kwargs = _consume_runtime_model(model, runtime_kwargs) user_config = _load_gateway_config() enabled_toolsets = sorted(_get_platform_tools(user_config, "api_server")) diff --git a/gateway/run.py b/gateway/run.py index 4f3b12375d6..63c5a8980ab 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1790,9 +1790,29 @@ def _resolve_runtime_agent_kwargs() -> dict: "args": list(runtime.get("args") or []), "credential_pool": runtime.get("credential_pool"), "max_tokens": max_tokens, + "model": runtime.get("model"), } +def _consume_runtime_model(model: str, runtime_kwargs: dict) -> tuple[str, dict]: + """Apply and remove a runtime-provider model from agent kwargs. + + Runtime provider resolution may supply an explicit model in addition to + credentials. AIAgent still receives ``model`` as a separate constructor + argument, so callers must consume this handoff key instead of forwarding it + through ``**runtime_kwargs`` where it would collide with ``model=...``. + """ + runtime_model = runtime_kwargs.pop("model", None) + if runtime_model: + logger.info( + "Runtime provider supplied explicit model override: %s -> %s", + model, + runtime_model, + ) + model = runtime_model + return model, runtime_kwargs + + def _try_resolve_fallback_provider() -> dict | None: """Attempt to resolve credentials from the fallback_model/fallback_providers config.""" from hermes_cli.runtime_provider import resolve_runtime_provider @@ -3358,14 +3378,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew ) runtime_kwargs = _resolve_runtime_agent_kwargs() - runtime_model = runtime_kwargs.pop("model", None) - if runtime_model: - logger.info( - "Runtime provider supplied explicit model override: %s -> %s", - model, - runtime_model, - ) - model = runtime_model + model, runtime_kwargs = _consume_runtime_model(model, runtime_kwargs) if override and resolved_session_key: model, runtime_kwargs = self._apply_session_model_override( resolved_session_key, model, runtime_kwargs @@ -8753,6 +8766,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew from agent.model_metadata import get_model_context_length _msg_cwd = os.environ.get("TERMINAL_CWD", os.path.expanduser("~")) + # Probe-only use: we read base_url/provider/api_key fields for + # context-length/credential resolution and never splat this dict + # into AIAgent, so the "model" key it now carries is ignored here + # (no _consume_runtime_model needed). See _consume_runtime_model. _msg_runtime = _resolve_runtime_agent_kwargs() _msg_config_ctx = None try: diff --git a/plugins/platforms/feishu/feishu_comment.py b/plugins/platforms/feishu/feishu_comment.py index 83b41469fdd..45c31529a25 100644 --- a/plugins/platforms/feishu/feishu_comment.py +++ b/plugins/platforms/feishu/feishu_comment.py @@ -979,8 +979,9 @@ def _resolve_model_and_runtime() -> Tuple[str, dict]: user_config = _load_gateway_config() model = _resolve_gateway_model(user_config) - from gateway.run import _resolve_runtime_agent_kwargs + from gateway.run import _consume_runtime_model, _resolve_runtime_agent_kwargs runtime_kwargs = _resolve_runtime_agent_kwargs() + model, runtime_kwargs = _consume_runtime_model(model, runtime_kwargs) # Fall back to provider's default model if none configured if not model and runtime_kwargs.get("provider"): diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index a941d4afc93..14d81f94e3c 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -337,6 +337,41 @@ class TestAdapterInit: assert isinstance(agent, FakeAgent) assert captured["reasoning_config"] == {"enabled": True, "effort": "xhigh"} + def test_create_agent_consumes_runtime_provider_model(self, monkeypatch): + captured = {} + + class FakeAgent: + def __init__(self, **kwargs): + captured.update(kwargs) + + monkeypatch.setattr("run_agent.AIAgent", FakeAgent) + monkeypatch.setattr( + "gateway.run._resolve_runtime_agent_kwargs", + lambda: { + "provider": "custom-runtime", + "base_url": "https://example.test/v1", + "api_mode": "openai", + "model": "runtime/model-from-provider", + }, + ) + monkeypatch.setattr("gateway.run._resolve_gateway_model", lambda: "") + monkeypatch.setattr("gateway.run._load_gateway_config", lambda: {}) + monkeypatch.setattr( + "gateway.run.GatewayRunner._load_reasoning_config", + staticmethod(lambda: {}), + ) + monkeypatch.setattr("gateway.run.GatewayRunner._load_fallback_model", staticmethod(lambda: None)) + monkeypatch.setattr("hermes_cli.tools_config._get_platform_tools", lambda *_: set()) + + adapter = APIServerAdapter(PlatformConfig(enabled=True)) + monkeypatch.setattr(adapter, "_ensure_session_db", lambda: None) + + agent = adapter._create_agent(session_id="api-session") + + assert isinstance(agent, FakeAgent) + assert captured["model"] == "runtime/model-from-provider" + assert captured["provider"] == "custom-runtime" + def test_create_agent_refreshes_max_iterations_from_runtime_config(self, monkeypatch): captured = {} diff --git a/tests/gateway/test_runtime_provider_model_handoff.py b/tests/gateway/test_runtime_provider_model_handoff.py new file mode 100644 index 00000000000..0ee6ea72c43 --- /dev/null +++ b/tests/gateway/test_runtime_provider_model_handoff.py @@ -0,0 +1,102 @@ +from gateway import run as gateway_run + + +def _runner(): + runner = gateway_run.GatewayRunner.__new__(gateway_run.GatewayRunner) + runner._session_model_overrides = {} + runner._last_resolved_model = {} + return runner + + +def test_runtime_agent_kwargs_preserves_explicit_provider_model(monkeypatch): + def fake_resolve_runtime_provider(): + return { + "api_key": "sk-test", + "base_url": "https://example.test/v1", + "provider": "custom-runtime", + "api_mode": "openai", + "model": "runtime/model-from-provider", + } + + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + fake_resolve_runtime_provider, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider._get_model_config", + lambda: {"default": ""}, + ) + + kwargs = gateway_run._resolve_runtime_agent_kwargs() + + assert kwargs["provider"] == "custom-runtime" + assert kwargs["model"] == "runtime/model-from-provider" + + +def test_session_runtime_uses_provider_model_when_config_model_empty(monkeypatch): + def fake_resolve_runtime_provider(): + return { + "api_key": "sk-test", + "base_url": "https://example.test/v1", + "provider": "custom-runtime", + "api_mode": "openai", + "model": "runtime/model-from-provider", + } + + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + fake_resolve_runtime_provider, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider._get_model_config", + lambda: {"default": ""}, + ) + monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda _cfg=None: "") + + model, runtime_kwargs = _runner()._resolve_session_agent_runtime(session_key="sid") + + assert model == "runtime/model-from-provider" + assert "model" not in runtime_kwargs + assert runtime_kwargs["provider"] == "custom-runtime" + + +def test_consume_runtime_model_leaves_kwargs_safe_for_agent_constructor(): + model, runtime_kwargs = gateway_run._consume_runtime_model( + "", + { + "provider": "custom-runtime", + "model": "runtime/model-from-provider", + }, + ) + + assert model == "runtime/model-from-provider" + assert runtime_kwargs == {"provider": "custom-runtime"} + + +def test_session_runtime_keeps_provider_default_fallback_without_runtime_model(monkeypatch): + def fake_resolve_runtime_provider(): + return { + "api_key": "sk-test", + "base_url": "https://example.test/v1", + "provider": "custom-runtime", + "api_mode": "openai", + } + + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + fake_resolve_runtime_provider, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider._get_model_config", + lambda: {"default": ""}, + ) + monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda _cfg=None: "") + monkeypatch.setattr( + "hermes_cli.models.get_default_model_for_provider", + lambda provider: "catalog/default" if provider == "custom-runtime" else "", + ) + + model, runtime_kwargs = _runner()._resolve_session_agent_runtime(session_key="sid") + + assert model == "catalog/default" + assert runtime_kwargs["provider"] == "custom-runtime"