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>
This commit is contained in:
JD Davis 2026-07-12 16:11:28 +00:00 committed by GitHub
parent d6259b2263
commit 9c7b9d5a9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 14 deletions

View file

@ -60,6 +60,12 @@ from headroom.proxy.tool_injection_config import (
from headroom.proxy.tool_injection_config import (
get_tool_tracker_max_sessions as _get_tool_tracker_max_sessions,
)
from headroom.proxy.tool_injection_logging import (
ToolInjectionDecision,
)
from headroom.proxy.tool_injection_logging import (
log_tool_injection_decision as _log_tool_injection_decision,
)
from headroom.proxy.tool_injection_tracker import SessionToolTracker as _SessionToolTracker
from headroom.proxy.tool_name_policy import extract_tool_name
@ -1930,12 +1936,7 @@ def log_tool_injection_decision(
*,
provider: str,
session_id: str | None,
decision: Literal[
"inject_first_time",
"inject_sticky_replay",
"skip",
"skip_disabled_via_env",
],
decision: ToolInjectionDecision,
tool_definition_bytes_count: int,
request_id: str | None,
) -> None:
@ -1947,14 +1948,13 @@ def log_tool_injection_decision(
tool definition contents (might contain user-specific schemas) per
constraint #11.
"""
logger.info(
"event=tool_injection_decision provider=%s session_id=%s "
"decision=%s tool_definition_bytes_count=%d request_id=%s",
provider,
session_id or "",
decision,
tool_definition_bytes_count,
request_id or "",
_log_tool_injection_decision(
logger=logger,
provider=provider,
session_id=session_id,
decision=decision,
tool_definition_bytes_count=tool_definition_bytes_count,
request_id=request_id,
)

View file

@ -0,0 +1,35 @@
"""Logging policy for proxy tool-injection decisions."""
from __future__ import annotations
import logging
from typing import Literal
ToolInjectionDecision = Literal[
"inject_first_time",
"inject_sticky_replay",
"skip",
"skip_disabled_via_env",
]
def log_tool_injection_decision(
*,
logger: logging.Logger,
provider: str,
session_id: str | None,
decision: ToolInjectionDecision,
tool_definition_bytes_count: int,
request_id: str | None,
) -> None:
"""Emit a cache-affecting tool-injection decision without tool contents."""
logger.info(
"event=tool_injection_decision provider=%s session_id=%s "
"decision=%s tool_definition_bytes_count=%d request_id=%s",
provider,
session_id or "",
decision,
tool_definition_bytes_count,
request_id or "",
)

View file

@ -0,0 +1,48 @@
from __future__ import annotations
import logging
import pytest
from headroom.proxy.helpers import log_tool_injection_decision as helper_log_tool_injection_decision
from headroom.proxy.tool_injection_logging import log_tool_injection_decision
def test_logs_tool_injection_decision_without_contents(caplog: pytest.LogCaptureFixture) -> None:
logger = logging.getLogger("headroom.proxy.test_tool_injection_logging")
with caplog.at_level(logging.INFO, logger=logger.name):
log_tool_injection_decision(
logger=logger,
provider="anthropic",
session_id="session-1",
decision="inject_sticky_replay",
tool_definition_bytes_count=123,
request_id="req-1",
)
assert len(caplog.records) == 1
message = caplog.records[0].getMessage()
assert "event=tool_injection_decision" in message
assert "provider=anthropic" in message
assert "session_id=session-1" in message
assert "decision=inject_sticky_replay" in message
assert "tool_definition_bytes_count=123" in message
assert "request_id=req-1" in message
assert "memory_save" not in message
assert "headroom_retrieve" not in message
def test_helper_wrapper_uses_proxy_logger(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.INFO, logger="headroom.proxy"):
helper_log_tool_injection_decision(
provider="openai",
session_id=None,
decision="skip",
tool_definition_bytes_count=0,
request_id=None,
)
assert len(caplog.records) == 1
assert caplog.records[0].name == "headroom.proxy"
assert "session_id= decision=skip" in caplog.records[0].getMessage()