Skip to content

Commit 9c7b9d5

Browse files
refactor(proxy): extract tool injection logging (#2009)
## Description Extracts proxy tool-injection decision logging from `headroom.proxy.helpers` into a focused logging policy module. The public helper function remains in place and delegates to the new module, so existing injection call sites keep their current API while the logging format has direct tests. Closes # ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Added `headroom.proxy.tool_injection_logging` with the shared `ToolInjectionDecision` type and structured logging helper. - Updated `helpers.log_tool_injection_decision` to delegate to the logging policy module while preserving the existing helper API. - Added tests that assert the emitted structured fields and verify tool names/contents are not logged. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text python -m pytest tests/test_tool_injection_logging.py tests/test_memory_tool_session_sticky.py tests/test_ccr_tool_always_on.py tests/test_corrupt_golden_bytes_recovery.py tests/test_issue_728_empty_tools_injection.py 60 passed in 0.95s python -m ruff check . All checks passed! python -m ruff format --check . 1069 files already formatted python -m mypy headroom --ignore-missing-imports Success: no issues found in 410 source files gitleaks protect --staged --no-banner --redact no leaks found ``` ## Real Behavior Proof - Environment: Windows, Python 3.13.13, clean worktree from `headroomlabs/main` at `d2170b19`. - Exact command / steps: Ran targeted logging, memory injection, CCR injection, corrupt-byte, and empty-tool regression tests plus ruff, ruff-format, mypy, and staged gitleaks scan. - Observed result: All targeted tests and local gates passed; staged secret scan found no leaks. - Not tested: Full Docker/native wrapper CI locally; covered by repository CI. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes Documentation and changelog updates are not applicable for this internal refactor. The push reported existing default-branch Dependabot vulnerabilities; this PR's staged gitleaks scan passed and CI security checks are expected to validate the branch. Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
1 parent d6259b2 commit 9c7b9d5

3 files changed

Lines changed: 97 additions & 14 deletions

File tree

headroom/proxy/helpers.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
from headroom.proxy.tool_injection_config import (
6161
get_tool_tracker_max_sessions as _get_tool_tracker_max_sessions,
6262
)
63+
from headroom.proxy.tool_injection_logging import (
64+
ToolInjectionDecision,
65+
)
66+
from headroom.proxy.tool_injection_logging import (
67+
log_tool_injection_decision as _log_tool_injection_decision,
68+
)
6369
from headroom.proxy.tool_injection_tracker import SessionToolTracker as _SessionToolTracker
6470
from headroom.proxy.tool_name_policy import extract_tool_name
6571

@@ -1930,12 +1936,7 @@ def log_tool_injection_decision(
19301936
*,
19311937
provider: str,
19321938
session_id: str | None,
1933-
decision: Literal[
1934-
"inject_first_time",
1935-
"inject_sticky_replay",
1936-
"skip",
1937-
"skip_disabled_via_env",
1938-
],
1939+
decision: ToolInjectionDecision,
19391940
tool_definition_bytes_count: int,
19401941
request_id: str | None,
19411942
) -> None:
@@ -1947,14 +1948,13 @@ def log_tool_injection_decision(
19471948
tool definition contents (might contain user-specific schemas) per
19481949
constraint #11.
19491950
"""
1950-
logger.info(
1951-
"event=tool_injection_decision provider=%s session_id=%s "
1952-
"decision=%s tool_definition_bytes_count=%d request_id=%s",
1953-
provider,
1954-
session_id or "",
1955-
decision,
1956-
tool_definition_bytes_count,
1957-
request_id or "",
1951+
_log_tool_injection_decision(
1952+
logger=logger,
1953+
provider=provider,
1954+
session_id=session_id,
1955+
decision=decision,
1956+
tool_definition_bytes_count=tool_definition_bytes_count,
1957+
request_id=request_id,
19581958
)
19591959

19601960

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Logging policy for proxy tool-injection decisions."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import Literal
7+
8+
ToolInjectionDecision = Literal[
9+
"inject_first_time",
10+
"inject_sticky_replay",
11+
"skip",
12+
"skip_disabled_via_env",
13+
]
14+
15+
16+
def log_tool_injection_decision(
17+
*,
18+
logger: logging.Logger,
19+
provider: str,
20+
session_id: str | None,
21+
decision: ToolInjectionDecision,
22+
tool_definition_bytes_count: int,
23+
request_id: str | None,
24+
) -> None:
25+
"""Emit a cache-affecting tool-injection decision without tool contents."""
26+
27+
logger.info(
28+
"event=tool_injection_decision provider=%s session_id=%s "
29+
"decision=%s tool_definition_bytes_count=%d request_id=%s",
30+
provider,
31+
session_id or "",
32+
decision,
33+
tool_definition_bytes_count,
34+
request_id or "",
35+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
import pytest
6+
7+
from headroom.proxy.helpers import log_tool_injection_decision as helper_log_tool_injection_decision
8+
from headroom.proxy.tool_injection_logging import log_tool_injection_decision
9+
10+
11+
def test_logs_tool_injection_decision_without_contents(caplog: pytest.LogCaptureFixture) -> None:
12+
logger = logging.getLogger("headroom.proxy.test_tool_injection_logging")
13+
14+
with caplog.at_level(logging.INFO, logger=logger.name):
15+
log_tool_injection_decision(
16+
logger=logger,
17+
provider="anthropic",
18+
session_id="session-1",
19+
decision="inject_sticky_replay",
20+
tool_definition_bytes_count=123,
21+
request_id="req-1",
22+
)
23+
24+
assert len(caplog.records) == 1
25+
message = caplog.records[0].getMessage()
26+
assert "event=tool_injection_decision" in message
27+
assert "provider=anthropic" in message
28+
assert "session_id=session-1" in message
29+
assert "decision=inject_sticky_replay" in message
30+
assert "tool_definition_bytes_count=123" in message
31+
assert "request_id=req-1" in message
32+
assert "memory_save" not in message
33+
assert "headroom_retrieve" not in message
34+
35+
36+
def test_helper_wrapper_uses_proxy_logger(caplog: pytest.LogCaptureFixture) -> None:
37+
with caplog.at_level(logging.INFO, logger="headroom.proxy"):
38+
helper_log_tool_injection_decision(
39+
provider="openai",
40+
session_id=None,
41+
decision="skip",
42+
tool_definition_bytes_count=0,
43+
request_id=None,
44+
)
45+
46+
assert len(caplog.records) == 1
47+
assert caplog.records[0].name == "headroom.proxy"
48+
assert "session_id= decision=skip" in caplog.records[0].getMessage()

0 commit comments

Comments
 (0)