From 38a562665880238cfb99d295ddd7baa762700d0e Mon Sep 17 00:00:00 2001 From: Dustin <204417361+Koraji95-coder@users.noreply.github.com> Date: Sat, 23 May 2026 15:46:58 -0500 Subject: [PATCH 1/2] fix(api-server): unblock /v1/chat/completions with narrow kwargs-collision fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `api_server` platform's `_create_agent()` builds `runtime_kwargs` via `_resolve_runtime_agent_kwargs()` (which sources `model` from config.yaml among other things), then constructs `AIAgent(model=model, **runtime_kwargs)` — which collides on the `model` key and raises `TypeError: AIAgent() got multiple values for keyword argument 'model'` before any request can be served. Every `POST /v1/chat/completions` 500s regardless of payload, making the OpenAI-compat API server unusable for any client. Repro (Windows + Linux both, hermes-agent v0.14.0): ~/.hermes/.env: API_SERVER_ENABLED=true API_SERVER_KEY=test-key hermes gateway curl -H "Authorization: Bearer test-key" \ -H "Content-Type: application/json" \ http://localhost:8642/v1/chat/completions \ -d '{"model":"hermes-agent","messages":[{"role":"user","content":"x"}]}' HTTP/1.1 500 Internal Server Error {"error": {"message": "Internal server error: run_agent.AIAgent() got multiple values for keyword argument 'model'", ...}} The fix is one line: pop `model` out of `runtime_kwargs` before the constructor, so the explicit `model=model` kwarg wins unambiguously. Semantics-preserving relative to the existing implicit behavior — the request-level `model` field is still ignored in favor of config.yaml's `model`, matching the current state of #10773 (the per-request-model design still being worked out across #25552 / #16403 / #18549 / #5862). Once #10773 lands, this `pop` gets replaced by whatever per-request routing it specifies; until then, this restores a working API server for any user out there. Strictly less than #25552 et al. — no per-request routing, no new config surface, no behavior change for anyone whose request payload already happened to match the server-side model. Just fixes the crash. Validated end-to-end on two production deployments running v0.14.0 behind a Tailscale-mesh reverse proxy: `POST /v1/chat/completions` returns `200` with a real OpenAI Chat Completion response body (real agent tool-calls, real responses, `finish_reason: stop`) where before this fix every request 500'd within ~50ms. See #10773 (root issue) for the broader design conversation. Pinged the maintainer team in the comment thread linking back to this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- gateway/platforms/api_server.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 0668896e170f..d1267a9ef512 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -878,6 +878,15 @@ def _create_agent( from hermes_cli.tools_config import _get_platform_tools runtime_kwargs = _resolve_runtime_agent_kwargs() + # Defer to the server-side `config.yaml` model rather than letting + # the kwarg below collide with a `model` key already populated in + # `runtime_kwargs`. Without this pop, the explicit `model=model` + # on the `AIAgent(...)` constructor a few lines down raises + # `TypeError: AIAgent() got multiple values for keyword argument + # 'model'` and 500s every `POST /v1/chat/completions` regardless + # of payload. See #10773 for the per-request-model design; this is + # the narrow band-aid only. + runtime_kwargs.pop('model', None) reasoning_config = GatewayRunner._load_reasoning_config() model = _resolve_gateway_model() From 86d349a030bb3fadeeb30d653aef8f03486eb08c Mon Sep 17 00:00:00 2001 From: Dustin <204417361+Koraji95-coder@users.noreply.github.com> Date: Sat, 23 May 2026 16:06:42 -0500 Subject: [PATCH 2/2] test(gateway): regression test for api_server model-kwargs collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `test_create_agent_pops_colliding_model_from_runtime_kwargs` to `tests/gateway/test_api_server.py::TestCreateAgent` covering the exact bug this PR fixes. The test mounts a `FakeAgent` via monkeypatch and puts a colliding `model` key inside `runtime_kwargs`, mirroring the real-world shape that triggered the original `TypeError: AIAgent() got multiple values for keyword argument 'model'`. Without the `runtime_kwargs.pop('model', None)` fix at api_server.py line 881, this test fails with the same TypeError that 500'd every `POST /v1/chat/completions` in production. With the fix, the test passes and asserts (a) the constructor receives exactly one `model` value, sourced from `_resolve_gateway_model()` not from the colliding key, and (b) other `runtime_kwargs` keys (e.g. `provider`) come through unaffected. Pattern matches the existing `test_create_agent_forwards_config_reasoning_effort` test in the same class — same `FakeAgent` mock shape, same monkeypatch targets, same `adapter._create_agent` invocation. Hermetic; no live network, no filesystem, no API keys. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/gateway/test_api_server.py | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index aae5f5505320..01ae338c9f93 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -305,6 +305,67 @@ def __init__(self, **kwargs): assert isinstance(agent, FakeAgent) assert captured["reasoning_config"] == {"enabled": True, "effort": "xhigh"} + def test_create_agent_pops_colliding_model_from_runtime_kwargs(self, monkeypatch): + """Regression test for the model-kwarg collision: + `_resolve_runtime_agent_kwargs()` sources `model` from + `config.yaml`, so `runtime_kwargs` can carry a `model` key. + Before the fix, `AIAgent(model=model, **runtime_kwargs)` then raised + `TypeError: AIAgent() got multiple values for keyword argument + 'model'` and every `POST /v1/chat/completions` returned HTTP 500. + + After the fix, the colliding key is popped out of `runtime_kwargs` + before the constructor call, so the explicit `model=model` (from + `_resolve_gateway_model()`) wins unambiguously and the request + flows. See PR #31139 / issue #10773 for the broader per-request- + model design discussion this band-aid sits beneath. + """ + captured = {} + + class FakeAgent: + def __init__(self, **kwargs): + captured.update(kwargs) + + monkeypatch.setattr("run_agent.AIAgent", FakeAgent) + # runtime_kwargs DOES carry a colliding `model` key — this is the + # exact shape that triggered the TypeError before the fix. + monkeypatch.setattr( + "gateway.run._resolve_runtime_agent_kwargs", + lambda: { + "provider": "openai-codex", + "model": "model-from-runtime-kwargs-this-collides", + }, + ) + monkeypatch.setattr( + "gateway.run._resolve_gateway_model", + lambda: "model-from-gateway-resolver", + ) + 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) + + # This call would raise TypeError before the fix. + agent = adapter._create_agent(session_id="api-session") + + assert isinstance(agent, FakeAgent) + # The explicit model=model kwarg (from _resolve_gateway_model) wins; + # the colliding runtime_kwargs.model was popped before the call. + assert captured["model"] == "model-from-gateway-resolver", ( + "Explicit model=model kwarg must win; runtime_kwargs.model was the " + "colliding source that gets popped." + ) + # provider should still come through from runtime_kwargs unaffected. + assert captured["provider"] == "openai-codex" + # --------------------------------------------------------------------------- # Auth checking