feat: add transform_api_request plugin hook for full-payload request transformation - #43241
feat: add transform_api_request plugin hook for full-payload request transformation#43241raymondclowe wants to merge 1 commit into
Conversation
…transformation Adds a new plugin hook that fires once per API call, receiving the mutable api_kwargs dict by reference. Unlike pre_api_request (observational), this hook allows plugins to modify messages, model, and parameters before they reach the provider. Changes: - hermes_cli/plugins.py: add transform_api_request to VALID_HOOKS - agent/conversation_loop.py: invoke hook between pre_api_request and the actual client API call - hermes_cli/hooks.py: add test-fixture kwargs Use cases: context compression (Headroom), security scanning, provider format translation, cost-aware model routing. Closes NousResearch#43237
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new mutative plugin hook (transform_api_request) to allow plugins to modify the outgoing provider request payload (api_kwargs) immediately before dispatch.
Changes:
- Registers
transform_api_requestas a supported plugin hook. - Invokes
transform_api_requestinrun_conversationwith a mutableapi_kwargsreference and request metadata. - Adds a regression test covering hook invocation and payload shape.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tests/run_agent/test_run_agent.py | Adds a test intended to validate that plugins can mutate api_kwargs before the provider call. |
| hermes_cli/plugins.py | Registers transform_api_request as a recognized hook name. |
| hermes_cli/hooks.py | Documents/example-payload update for the new hook in the hooks listing output. |
| agent/conversation_loop.py | Calls the new mutative hook immediately before the API request is sent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if has_hook("transform_api_request"): | ||
| _invoke_hook( | ||
| "transform_api_request", | ||
| api_kwargs=api_kwargs, | ||
| session_id=agent.session_id or "", | ||
| task_id=effective_task_id, | ||
| turn_id=turn_id, | ||
| model=agent.model, | ||
| provider=agent.provider, | ||
| base_url=agent.base_url, | ||
| api_mode=agent.api_mode, | ||
| api_call_count=api_call_count, | ||
| api_request_id=api_request_id, | ||
| ) |
| "transform_api_request": { | ||
| "session_id": "test-session", | ||
| "task_id": "test-task", | ||
| "model": "claude-sonnet-4-6", | ||
| "provider": "anthropic", | ||
| "base_url": "https://api.anthropic.com", | ||
| "api_mode": "anthropic_messages", | ||
| "api_call_count": 1, | ||
| "turn_id": "test-turn", | ||
| "request": {"body": {"messages": [{"role": "user", "content": "hello"}]}}, | ||
| }, |
| def test_transform_api_request_hook_mutates_api_kwargs(self, agent): | ||
| """The transform_api_request hook receives mutable api_kwargs and | ||
| plugins can modify messages before they reach the provider.""" | ||
| self._setup_agent(agent) | ||
| resp = _mock_response(content="Compressed response", finish_reason="stop") | ||
| agent.client.chat.completions.create.return_value = resp | ||
|
|
||
| hook_received_kwargs = {} | ||
| transform_called = False | ||
|
|
||
| def _transform_hook(name, **kwargs): | ||
| nonlocal transform_called, hook_received_kwargs | ||
| if name == "transform_api_request": | ||
| hook_received_kwargs = dict(kwargs) | ||
| # Simulate a compression plugin: strip tool outputs | ||
| api_kw = kwargs.get("api_kwargs", {}) | ||
| msgs = api_kw.get("messages", []) | ||
| for m in msgs: | ||
| if m.get("role") == "tool" and m.get("content"): | ||
| m["content"] = "[compressed]" | ||
| transform_called = True | ||
| return [] |
| "post_llm_call", | ||
| "pre_api_request", | ||
| "post_api_request", | ||
| "transform_api_request", | ||
| "api_request_error", |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Looks Good:
- Adds a new mutative plugin hook called transform_api_request that fires once per API call, receives the mutable api_kwargs dict by reference, and lets plugins modify messages, model, and other parameters before the request is sent. This is a clean extension point for context compression, security scanning, format translation, and cost-aware model routing.
- Correctly documented as mutative (unlike pre_api_request which is observational and shallow-copied).
- Test verifies the hook receives api_kwargs, can mutate messages, and the mutation flows to the provider call.
- Hook registered in plugins.py, documented in hooks.py example payload.
- Only 4 files, +86/-0. Clean feature addition.
Reviewed by Hermes Agent
|
Thanks for the focused plugin-extension proposal. Automated hermes-sweeper review found that the requested behavior is already available on current
A plugin needing the proposed Headroom-style behavior can register |
Summary
Adds a
transform_api_requestplugin hook that fires once per API call, receiving the mutableapi_kwargsdict by reference — the same dict passed to the provider client on the next line.Unlike
pre_api_request(observational, shallow-copied messages), this hook is mutative: plugins can modify messages, model, and other parameters before they reach the provider. Mutations are ephemeral — they affect only the current API call, never the persisted session DB.Motivation
Closes #43237. See that issue for detailed rationale, use cases, and comparison to the related
transform_api_messagehook (#20307).Use cases enabled:
Changes
hermes_cli/plugins.pytransform_api_requesttoVALID_HOOKSagent/conversation_loop.pypre_api_requestand the actual client API call, passingapi_kwargsby mutable referencehermes_cli/hooks.pytests/run_agent/test_run_agent.pyapi_kwargsand can modify messagesTest Plan
test_transform_api_request_hook_mutates_api_kwargs— verifies hook receivesapi_kwargs, can mutate messages, metadata fields presentTestRunConversationsuite — all 122 tests passTestPluginHookssuite — all tests passpre_api_request/post_api_requesthook testsScope
~86 lines across 4 files. The integration point already exists — this adds a hook invocation in the gap between
pre_api_requestand the streaming/non-streaming API call.