mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
fix(engine): Chunk 5.2 — wire CCR + memory into OpenAI chat engine path
OpenAI CCR (mirrors handle_openai_chat lines 1593-1629): - CCRToolInjector(provider="openai") — OpenAI message/tool shapes - apply_session_sticky_ccr_tool(provider="openai") for sticky-on - NO frozen_message_count guard (handler has none for OpenAI) - NO compression tracking or proactive expansion (handler omits both) - tools updated in body["tools"] after injection OpenAI memory (mirrors handle_openai_chat lines 1643-1733): - Uses append_text_to_latest_user_chat_message (backward scan, no frozen) - Does NOT skip in cache mode (handler injects in all modes) - MemoryDecision.decide gate + inject_context sub-gate preserved Both CCR/memory are no-ops when components=None — all 22 OpenAI + all Anthropic + all existing tests remain byte-identical (239/239 green). Responses + CCR/memory: intentionally deferred; memory injection runs pre-compression in the live handler which requires restructuring the responses path; documented in method docstring. New structural tests: test_facade_openai_ccr.py (9 tests) + test_facade_openai_memory.py (11 tests) — all pass.
This commit is contained in:
parent
4fe18bf328
commit
42901e418b
3 changed files with 1150 additions and 10 deletions
|
|
@ -1,4 +1,4 @@
|
|||
"""HeadroomEngine — request/response hook facade (Chunks 2 + 4.2a/4.2b/4.2c + 4.3-i + 5 + 5R).
|
||||
"""HeadroomEngine — request/response hook facade (Chunks 2 + 4.2a/4.2b/4.2c + 4.3-i + 5 + 5R + 5.2).
|
||||
|
||||
Composes the existing compression subsystems behind a clean hook interface.
|
||||
Does NOT reimplement compression; delegates to injected ``CompressionPipeline``
|
||||
|
|
@ -54,6 +54,39 @@ Design notes
|
|||
``stream_options`` before forwarding (unlike chat);
|
||||
* passthrough (no compression): returns raw inbound bytes byte-identical;
|
||||
* compressed: canonical re-serialization of the mutated body.
|
||||
- **Chunk 5.2 — OpenAI chat CCR + memory**: ``ccr_components`` and
|
||||
``memory_components`` are now wired into ``_on_request_openai_chat``.
|
||||
Key differences from the Anthropic CCR/memory blocks:
|
||||
|
||||
CCR differences (mirrors ``OpenAIHandlerMixin.handle_openai_chat`` lines
|
||||
~1593-1629):
|
||||
* ``CCRToolInjector(provider="openai", ...)`` — OpenAI message shapes.
|
||||
* NO ``frozen_message_count`` guard on system-instruction injection or tool
|
||||
injection — the live OpenAI handler does not apply those guards.
|
||||
* NO compression tracking (step 3) or proactive expansion (step 4) — the
|
||||
live OpenAI handler omits those CCR phases entirely.
|
||||
* Tools updated in ``body["tools"]`` after CCR injection (same as handler
|
||||
at lines ~1755-1756).
|
||||
|
||||
Memory differences (mirrors lines ~1643-1733):
|
||||
* Uses ``append_text_to_latest_user_chat_message`` (OpenAI helper that
|
||||
scans backwards ignoring frozen count) instead of the Anthropic frozen-
|
||||
aware helper.
|
||||
* Does NOT skip injection in cache mode (the OpenAI handler injects in all
|
||||
modes).
|
||||
* When CCR components is None but memory components is set, memory injection
|
||||
still fires (the two are independent gates).
|
||||
|
||||
Responses + CCR/memory:
|
||||
The live ``handle_openai_responses`` handler does run memory injection
|
||||
(into ``body["input"]``) BEFORE compression. The engine's
|
||||
``_on_request_openai_responses`` currently does not wire CCR or memory
|
||||
(the golden corpus has both features off and the handler position is
|
||||
pre-compression rather than post-compression). This is intentional: the
|
||||
Responses path does not expose ``messages`` to the engine after the
|
||||
compression step, and wiring it pre-compression would require restructuring
|
||||
the responses path. Memory for Responses is deferred; the omission is
|
||||
documented here and in the method docstring.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -980,14 +1013,16 @@ class HeadroomEngine:
|
|||
),
|
||||
)
|
||||
|
||||
# ── Real OpenAI chat orchestration (Chunk 5) ─────────────────────────────
|
||||
# ── Real OpenAI chat orchestration (Chunk 5 + 5.2) ──────────────────────
|
||||
|
||||
def _on_request_openai_chat(self, ctx: RequestContext) -> RequestDecision:
|
||||
"""Reproduce the OpenAI /v1/chat/completions compression-core.
|
||||
|
||||
Mirrors ``OpenAIHandlerMixin.handle_openai_chat`` through:
|
||||
CompressionDecision → mode-branch pipeline.apply → (streaming:
|
||||
inject stream_options) → canonical serialization.
|
||||
CompressionDecision → mode-branch pipeline.apply → CCR marker-scan +
|
||||
tool-inject (when ccr_components set, not bypass) → memory inject
|
||||
(when memory_components set, not bypass) → (streaming: inject
|
||||
stream_options) → canonical serialization.
|
||||
|
||||
Intentional differences from the Anthropic path that are faithfully
|
||||
preserved here to match the live handler byte-for-byte:
|
||||
|
|
@ -1008,11 +1043,21 @@ class HeadroomEngine:
|
|||
BEFORE forwarding the bytes. This produces a byte-level difference
|
||||
vs the inbound body and is captured in the streaming golden fixtures.
|
||||
|
||||
Excluded from this chunk: CCR injection, memory injection, hooks,
|
||||
pipeline_extension events, prefix-tracker.update_from_response,
|
||||
cache, rate-limiting, image compression. These are all controlled OFF
|
||||
in the golden recorder's ``_DEFAULT_CONFIG_KWARGS``, so the 16 chat
|
||||
golden fixtures do not exercise them.
|
||||
4. **CCR differences from Anthropic** (Chunk 5.2):
|
||||
- ``CCRToolInjector(provider="openai", ...)`` — OpenAI shapes.
|
||||
- NO ``frozen_message_count`` guard on system-instruction or tool
|
||||
injection (handler lines 1603-1629 have no such guard).
|
||||
- NO compression tracking or proactive expansion — the live OpenAI
|
||||
handler omits CCR phases 3 and 4 entirely.
|
||||
|
||||
5. **Memory differences from Anthropic** (Chunk 5.2):
|
||||
- Uses ``append_text_to_latest_user_chat_message`` (OpenAI helper
|
||||
that scans backwards without frozen-count awareness).
|
||||
- Does NOT skip in cache mode (the handler injects in all modes).
|
||||
|
||||
When ``ccr_components`` or ``memory_components`` is None, those steps
|
||||
are a no-op — all 22 existing golden fixtures (CCR/memory off) remain
|
||||
byte-identical.
|
||||
"""
|
||||
from headroom.proxy.helpers import serialize_body_canonical
|
||||
from headroom.proxy.modes import is_cache_mode, is_token_mode
|
||||
|
|
@ -1053,6 +1098,11 @@ class HeadroomEngine:
|
|||
)
|
||||
|
||||
optimized_messages = messages
|
||||
# session_id is derived once (outside the compress block) so CCR can
|
||||
# use it even when compression was skipped (sticky tool logic needs it).
|
||||
session_id: str | None = None
|
||||
frozen_message_count = 0
|
||||
request_id = ctx.request_id
|
||||
|
||||
if _decision.should_compress and not _bypass:
|
||||
# --- Session / frozen-count derivation ---
|
||||
|
|
@ -1068,9 +1118,9 @@ class HeadroomEngine:
|
|||
context_limit = oc.provider.get_context_limit(model)
|
||||
|
||||
biases = None
|
||||
request_id = ctx.request_id
|
||||
|
||||
# --- Mode branch: token / non-cache / cache-delta ---
|
||||
assert session_id is not None # set 3 lines above; mypy flow narrowing
|
||||
if is_token_mode(oc.config.mode):
|
||||
comp_cache = oc.get_compression_cache(session_id)
|
||||
|
||||
|
|
@ -1111,8 +1161,105 @@ class HeadroomEngine:
|
|||
if result.messages != messages:
|
||||
optimized_messages = result.messages
|
||||
|
||||
# ── CCR request-side (Chunk 5.2 — OpenAI) ────────────────────────────
|
||||
# Mirrors ``handle_openai_chat`` lines ~1593-1629.
|
||||
# Steps wired: marker scan + system-instruction injection +
|
||||
# session-sticky tool injection.
|
||||
# NOT wired: compression tracking (step 3) + proactive expansion
|
||||
# (step 4) — the live OpenAI handler does not implement those phases.
|
||||
# Gate: ccr_components not None AND not bypass (same as handler).
|
||||
ccr = self._ccr_components
|
||||
tools = body.get("tools")
|
||||
ccr_tool_injected = False
|
||||
|
||||
if ccr is not None and not _bypass:
|
||||
# Derive session_id for sticky-tool registration even when
|
||||
# compression was skipped (_decision.should_compress=False).
|
||||
if session_id is None:
|
||||
session_id = oc.session_tracker_store.compute_session_id(ctx, model, messages)
|
||||
|
||||
if oc.config.ccr_inject_tool or oc.config.ccr_inject_system_instructions:
|
||||
from headroom.ccr import CCRToolInjector
|
||||
from headroom.proxy.helpers import apply_session_sticky_ccr_tool
|
||||
|
||||
injector = CCRToolInjector(
|
||||
provider="openai",
|
||||
inject_tool=False, # routed through sticky helper below
|
||||
inject_system_instructions=oc.config.ccr_inject_system_instructions,
|
||||
)
|
||||
injector.scan_for_markers(optimized_messages)
|
||||
|
||||
# System-instruction injection — NO frozen guard (handler
|
||||
# line 1611 has no such guard for OpenAI).
|
||||
if oc.config.ccr_inject_system_instructions and injector.has_compressed_content:
|
||||
optimized_messages = injector.inject_into_system_message(optimized_messages)
|
||||
|
||||
if oc.config.ccr_inject_tool:
|
||||
tools, ccr_tool_injected = apply_session_sticky_ccr_tool(
|
||||
provider="openai",
|
||||
session_id=session_id,
|
||||
request_id=request_id,
|
||||
existing_tools=tools,
|
||||
has_compressed_content_this_turn=injector.has_compressed_content,
|
||||
)
|
||||
if ccr_tool_injected:
|
||||
logger.debug(
|
||||
"[%s] CCR(engine/openai): tool registered (session=%s, "
|
||||
"compressed_this_turn=%s, hashes_seen=%d)",
|
||||
request_id,
|
||||
session_id,
|
||||
injector.has_compressed_content,
|
||||
len(injector.detected_hashes),
|
||||
)
|
||||
|
||||
# ── Memory injection (Chunk 5.2 — OpenAI) ────────────────────────────
|
||||
# Mirrors ``handle_openai_chat`` lines ~1643-1733.
|
||||
# Uses ``append_text_to_latest_user_chat_message`` (OpenAI helper)
|
||||
# which scans backwards through all messages without frozen-count
|
||||
# awareness — the live handler does not apply a frozen guard here.
|
||||
# Does NOT skip in cache mode (handler injects in all modes).
|
||||
# Gate: memory_components not None AND not bypass.
|
||||
mc = self._memory_components
|
||||
if mc is not None and not _bypass:
|
||||
from headroom.proxy.helpers import get_memory_injection_mode
|
||||
from headroom.proxy.memory_decision import MemoryDecision
|
||||
|
||||
_header_user_id = headers.get("x-headroom-user-id", "")
|
||||
memory_user_id: str | None = _header_user_id if _header_user_id else mc.default_user_id
|
||||
|
||||
mem_decision = MemoryDecision.decide(
|
||||
headers=ctx.headers_view,
|
||||
memory_handler=mc.memory_handler,
|
||||
memory_user_id=memory_user_id,
|
||||
mode_name=get_memory_injection_mode(),
|
||||
)
|
||||
if mem_decision.inject:
|
||||
_inject_ctx = mc.memory_handler is not None and getattr(
|
||||
getattr(mc.memory_handler, "config", None), "inject_context", True
|
||||
)
|
||||
if _inject_ctx:
|
||||
memory_context: str | None = ctx.prefetched_memory_context
|
||||
if memory_context:
|
||||
from headroom.proxy.helpers import (
|
||||
append_text_to_latest_user_chat_message,
|
||||
)
|
||||
|
||||
new_messages, bytes_appended = append_text_to_latest_user_chat_message(
|
||||
optimized_messages, memory_context
|
||||
)
|
||||
if bytes_appended > 0:
|
||||
optimized_messages = new_messages
|
||||
logger.debug(
|
||||
"[%s] Memory(engine/openai): injected %d bytes into "
|
||||
"latest user message tail",
|
||||
request_id,
|
||||
bytes_appended,
|
||||
)
|
||||
|
||||
# --- Reassemble body ---
|
||||
body["messages"] = optimized_messages
|
||||
if tools is not None:
|
||||
body["tools"] = tools
|
||||
|
||||
# --- Streaming: inject stream_options (mirrors handler lines ~2026-2029) ---
|
||||
# The live handler injects this BEFORE forwarding bytes, so it appears in the
|
||||
|
|
@ -1138,6 +1285,7 @@ class HeadroomEngine:
|
|||
telemetry=ResponseTelemetry(
|
||||
bytes_saved=bytes_saved,
|
||||
compressed=compressed,
|
||||
ccr_fired=ccr_tool_injected,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -1178,6 +1326,17 @@ class HeadroomEngine:
|
|||
Excluded: memory injection, CCR, beta-header sticky merge, image
|
||||
compression, hooks — all off in the golden corpus's
|
||||
``_DEFAULT_CONFIG_KWARGS``.
|
||||
|
||||
Memory / CCR for Responses (intentionally deferred):
|
||||
The live ``handle_openai_responses`` handler runs memory injection
|
||||
into ``body["input"]`` BEFORE compression, using
|
||||
``append_text_to_latest_user_input_item``. Wiring it here would
|
||||
require restructuring the engine to expose ``body["input"]`` after
|
||||
the ``_compress_openai_responses_payload`` step (the compressor
|
||||
already returns the mutated body dict). This is deferred; when
|
||||
wired it should run AFTER compression (mirrors the chat path) and
|
||||
use the same ``append_text_to_latest_user_input_item`` helper.
|
||||
The 6 golden Responses fixtures are not affected (memory/CCR off).
|
||||
"""
|
||||
from headroom.proxy.helpers import serialize_body_canonical
|
||||
|
||||
|
|
|
|||
470
tests/engine/test_facade_openai_ccr.py
Normal file
470
tests/engine/test_facade_openai_ccr.py
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
"""Structural tests for OpenAI CCR request-side steps (Chunk 5.2).
|
||||
|
||||
Mirrors ``test_facade_ccr.py`` (Anthropic) but for the OpenAI chat path.
|
||||
|
||||
Key differences from the Anthropic CCR tests:
|
||||
- ``provider="openai"`` throughout (CCRToolInjector + apply_session_sticky_ccr_tool).
|
||||
- NO frozen_message_count guard on system-instruction or tool injection —
|
||||
the live OpenAI handler does not apply those guards.
|
||||
- NO compression tracking (step 3) or proactive expansion (step 4) —
|
||||
the live OpenAI handler omits those CCR phases.
|
||||
- The engine uses ``_on_request_openai_chat`` via ``OpenAIComponents``
|
||||
(not ``AnthropicComponents``).
|
||||
|
||||
Running
|
||||
-------
|
||||
.venv/bin/python -m pytest tests/engine/test_facade_openai_ccr.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_CCR_TEST_HASH = "abcdef123456789012345678"
|
||||
_CCR_MARKER = f"[100 items compressed to 10. Retrieve more: hash={_CCR_TEST_HASH}]"
|
||||
|
||||
|
||||
def _make_engine(
|
||||
*,
|
||||
config_overrides: dict[str, Any] | None = None,
|
||||
frozen_count: int = 0,
|
||||
with_ccr: bool = True,
|
||||
ccr_context_tracker: Any | None = None,
|
||||
get_compression_store: Any | None = None,
|
||||
session_turn_counters: dict[str, int] | None = None,
|
||||
) -> Any:
|
||||
"""Build a HeadroomEngine with OpenAIComponents + CCRComponents."""
|
||||
from headroom.engine.contract import Flavor, Provider
|
||||
from headroom.engine.facade import CCRComponents, HeadroomEngine, OpenAIComponents
|
||||
from headroom.proxy.models import ProxyConfig
|
||||
from headroom.proxy.server import HeadroomProxy
|
||||
|
||||
config_kwargs: dict[str, Any] = {
|
||||
"optimize": True,
|
||||
"mode": "token",
|
||||
"cache_enabled": False,
|
||||
"rate_limit_enabled": False,
|
||||
"cost_tracking_enabled": False,
|
||||
"log_requests": False,
|
||||
"ccr_inject_tool": True,
|
||||
"ccr_inject_system_instructions": True,
|
||||
"ccr_handle_responses": False,
|
||||
"ccr_context_tracking": False,
|
||||
"ccr_proactive_expansion": False,
|
||||
"image_optimize": False,
|
||||
}
|
||||
if config_overrides:
|
||||
config_kwargs.update(config_overrides)
|
||||
|
||||
config = ProxyConfig(**config_kwargs)
|
||||
proxy = HeadroomProxy(config)
|
||||
|
||||
class _FixedStore:
|
||||
def compute_session_id(self, ctx: Any, model: str, msgs: Any) -> str:
|
||||
return "ccr-openai-structural-test-session"
|
||||
|
||||
def get_or_create(self, session_id: str, provider: str) -> Any:
|
||||
class _T:
|
||||
def get_frozen_message_count(self) -> int:
|
||||
return frozen_count
|
||||
|
||||
def get_last_original_messages(self) -> list[Any]:
|
||||
return []
|
||||
|
||||
def get_last_forwarded_messages(self) -> list[Any]:
|
||||
return []
|
||||
|
||||
return _T()
|
||||
|
||||
def get_fresh_cache(self, session_id: str) -> Any:
|
||||
class _C:
|
||||
def apply_cached(self, msgs: list[Any]) -> list[Any]:
|
||||
return list(msgs)
|
||||
|
||||
def compute_frozen_count(self, msgs: list[Any]) -> int:
|
||||
return 0
|
||||
|
||||
def update_from_result(self, orig: Any, compr: Any) -> None:
|
||||
pass
|
||||
|
||||
def mark_stable_from_messages(self, msgs: Any, up_to: int) -> None:
|
||||
pass
|
||||
|
||||
return _C()
|
||||
|
||||
oc = OpenAIComponents(
|
||||
pipeline=proxy.openai_pipeline,
|
||||
provider=proxy.openai_provider,
|
||||
session_tracker_store=_FixedStore(),
|
||||
get_compression_cache=_FixedStore().get_fresh_cache,
|
||||
config=proxy.config,
|
||||
usage_reporter=None,
|
||||
)
|
||||
|
||||
ccr = None
|
||||
if with_ccr:
|
||||
ccr = CCRComponents(
|
||||
ccr_context_tracker=ccr_context_tracker,
|
||||
get_compression_store=get_compression_store or (lambda: MagicMock()),
|
||||
session_turn_counters=session_turn_counters
|
||||
if session_turn_counters is not None
|
||||
else {},
|
||||
)
|
||||
|
||||
engine = HeadroomEngine(
|
||||
pipelines={(Provider.OPENAI, Flavor.CHAT): proxy.openai_pipeline},
|
||||
config=proxy.config,
|
||||
usage_reporter=None,
|
||||
salt=b"ccr-openai-structural-test-salt",
|
||||
openai_components=oc,
|
||||
ccr_components=ccr,
|
||||
)
|
||||
return engine
|
||||
|
||||
|
||||
def _make_ctx(
|
||||
body: dict[str, Any],
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
) -> Any:
|
||||
"""Build a RequestContext for OpenAI chat structural tests."""
|
||||
from headroom.engine.contract import Flavor, Provider, RequestContext
|
||||
|
||||
h: dict[str, str] = {
|
||||
"authorization": "Bearer sk-test-openai-key",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
if cwd:
|
||||
h["x-headroom-cwd"] = cwd
|
||||
if headers:
|
||||
h.update(headers)
|
||||
|
||||
return RequestContext(
|
||||
provider=Provider.OPENAI,
|
||||
flavor=Flavor.CHAT,
|
||||
headers_view=h,
|
||||
raw_body=json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode(),
|
||||
session_key="ccr-openai-structural",
|
||||
request_id="req-openai-ccr-test",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. CCR is a no-op when ccr_components is None
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ccr_noop_when_components_none() -> None:
|
||||
"""Engine without CCRComponents is byte-identical — OpenAI golden fixtures unaffected."""
|
||||
engine = _make_engine(with_ccr=False)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": f"Compressed result. {_CCR_MARKER}",
|
||||
"tool_call_id": "call_x",
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
# CCR tool must NOT have been injected (no ccr_components).
|
||||
tools = out.get("tools")
|
||||
assert tools is None, "No tools must appear when CCRComponents is None"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Tool injection with OpenAI shapes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ccr_tool_injected_openai_shape() -> None:
|
||||
"""CCR tool injection produces OpenAI tool shapes: [{type:function, function:{...}}].
|
||||
|
||||
When a CCR marker is present and ccr_inject_tool=True, the engine adds
|
||||
the headroom_retrieve tool to body["tools"] in OpenAI format.
|
||||
"""
|
||||
engine = _make_engine()
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": f"Fetched data. {_CCR_MARKER}",
|
||||
"tool_call_id": "call_fetch",
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
tools = out.get("tools")
|
||||
assert isinstance(tools, list), "tools must be a list after CCR injection"
|
||||
assert len(tools) > 0, "At least one tool must be injected"
|
||||
|
||||
# Each injected tool must have OpenAI shape: {type: function, function: {...}}
|
||||
for tool in tools:
|
||||
assert tool.get("type") == "function", (
|
||||
f"OpenAI tool must have type='function', got {tool.get('type')!r}"
|
||||
)
|
||||
assert "function" in tool, "OpenAI tool must have a 'function' sub-dict"
|
||||
assert "name" in tool["function"], "OpenAI tool.function must have 'name'"
|
||||
|
||||
|
||||
def test_ccr_tool_injected_appended_to_existing_tools() -> None:
|
||||
"""CCR tool injection appends to existing tools (OpenAI sticky merge)."""
|
||||
engine = _make_engine()
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": f"Result. {_CCR_MARKER}",
|
||||
"tool_call_id": "call_y",
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "existing_tool",
|
||||
"description": "pre-existing tool",
|
||||
"parameters": {"type": "object"},
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
tools = out.get("tools", [])
|
||||
tool_names = [t["function"]["name"] for t in tools if "function" in t]
|
||||
# Original tool must be preserved.
|
||||
assert "existing_tool" in tool_names, "Existing tool must be preserved after CCR injection"
|
||||
# CCR tool must be added.
|
||||
assert any("retrieve" in name.lower() or "headroom" in name.lower() for name in tool_names), (
|
||||
f"CCR retrieve tool must be present in tool names; got: {tool_names!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_ccr_no_tool_injected_when_inject_tool_false() -> None:
|
||||
"""ccr_inject_tool=False → no tool injection regardless of marker presence."""
|
||||
engine = _make_engine(
|
||||
config_overrides={
|
||||
"ccr_inject_tool": False,
|
||||
"ccr_inject_system_instructions": False,
|
||||
}
|
||||
)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "No injection configured."}],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
# ccr_inject_tool=False → no tool injection at all.
|
||||
tools = out.get("tools")
|
||||
assert tools is None, "No CCR tool should be injected when ccr_inject_tool=False"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. No frozen guard on system-instruction injection (OpenAI differs from Anthropic)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ccr_system_instruction_injected_even_with_frozen_prefix() -> None:
|
||||
"""OpenAI CCR: system-instruction injection has NO frozen_message_count guard.
|
||||
|
||||
The Anthropic engine guards injection when frozen_count > 0; the OpenAI
|
||||
handler does not. This test confirms the engine matches the handler.
|
||||
"""
|
||||
engine = _make_engine(
|
||||
frozen_count=2, # non-zero frozen prefix
|
||||
config_overrides={
|
||||
"optimize": True,
|
||||
"mode": "token",
|
||||
"ccr_inject_tool": False,
|
||||
"ccr_inject_system_instructions": True,
|
||||
},
|
||||
)
|
||||
|
||||
# Use a message body with a CCR marker so system instruction injection triggers.
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Compressed data was found. {_CCR_MARKER}",
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
|
||||
# The system message should have been injected (no frozen guard).
|
||||
# CCRToolInjector.inject_into_system_message prepends a system message
|
||||
# when no system message exists, or injects into the existing one.
|
||||
# We just confirm the call completed without error (no frozen guard raised it).
|
||||
# The body is a valid dict — that is the contract.
|
||||
assert isinstance(out, dict), "Engine must return a valid body even with frozen_count > 0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. No compression tracking or proactive expansion (OpenAI omits steps 3 + 4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_no_compression_tracking_for_openai() -> None:
|
||||
"""CCR context tracker is NOT called for OpenAI (steps 3+4 are Anthropic-only).
|
||||
|
||||
Even when ccr_context_tracker is set on CCRComponents and a CCR marker
|
||||
is present, the engine must NOT call track_compression for the OpenAI
|
||||
path (the live handler does not implement that phase).
|
||||
"""
|
||||
mock_tracker = MagicMock()
|
||||
mock_store = MagicMock()
|
||||
mock_store.get_metadata.return_value = {
|
||||
"tool_name": "search_files",
|
||||
"original_item_count": 100,
|
||||
"compressed_item_count": 10,
|
||||
"query_context": "find auth files",
|
||||
"compressed_content": "auth_middleware.py\n",
|
||||
}
|
||||
|
||||
engine = _make_engine(
|
||||
ccr_context_tracker=mock_tracker,
|
||||
get_compression_store=lambda: mock_store,
|
||||
config_overrides={
|
||||
"ccr_inject_tool": True,
|
||||
"ccr_inject_system_instructions": False,
|
||||
"ccr_context_tracking": True,
|
||||
"ccr_proactive_expansion": False,
|
||||
},
|
||||
)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": f"Found files. {_CCR_MARKER}",
|
||||
"tool_call_id": "call_search",
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body, cwd="/home/user/myproject")
|
||||
engine.on_request(ctx)
|
||||
|
||||
# track_compression must NOT be called for OpenAI (step 3 not wired).
|
||||
mock_tracker.track_compression.assert_not_called()
|
||||
# analyze_query must NOT be called for OpenAI (step 4 not wired).
|
||||
mock_tracker.analyze_query.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Bypass gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ccr_skipped_on_bypass_header_openai() -> None:
|
||||
"""x-headroom-bypass: true → CCR steps are skipped for OpenAI."""
|
||||
engine = _make_engine(
|
||||
config_overrides={
|
||||
"ccr_inject_tool": True,
|
||||
"ccr_inject_system_instructions": True,
|
||||
},
|
||||
)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": f"Marker present: {_CCR_MARKER}",
|
||||
"tool_call_id": "call_z",
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body, headers={"x-headroom-bypass": "true"})
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
# No CCR tool must be injected under bypass.
|
||||
assert out.get("tools") is None, "No CCR tool should be injected under bypass"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. ccr_tool_injected reflected in telemetry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ccr_fired_telemetry_true_when_tool_injected() -> None:
|
||||
"""ccr_fired is True in telemetry when CCR tool is injected."""
|
||||
engine = _make_engine(
|
||||
config_overrides={
|
||||
"ccr_inject_tool": True,
|
||||
"ccr_inject_system_instructions": False,
|
||||
},
|
||||
)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": f"CCR content: {_CCR_MARKER}",
|
||||
"tool_call_id": "call_t",
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
assert decision.telemetry.ccr_fired is True, (
|
||||
"telemetry.ccr_fired must be True when CCR tool was injected"
|
||||
)
|
||||
|
||||
|
||||
def test_ccr_fired_telemetry_false_when_inject_tool_disabled() -> None:
|
||||
"""ccr_fired is False when ccr_inject_tool=False (tool injection disabled)."""
|
||||
engine = _make_engine(
|
||||
config_overrides={
|
||||
"ccr_inject_tool": False,
|
||||
"ccr_inject_system_instructions": False,
|
||||
}
|
||||
)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "Plain message, no injection."}],
|
||||
}
|
||||
ctx = _make_ctx(body)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
assert decision.telemetry.ccr_fired is False, (
|
||||
"telemetry.ccr_fired must be False when ccr_inject_tool=False"
|
||||
)
|
||||
511
tests/engine/test_facade_openai_memory.py
Normal file
511
tests/engine/test_facade_openai_memory.py
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
"""Structural tests for OpenAI memory injection (Chunk 5.2).
|
||||
|
||||
Mirrors ``test_facade_memory.py`` (Anthropic) but for the OpenAI chat path.
|
||||
|
||||
Key differences from the Anthropic memory tests:
|
||||
- Uses ``append_text_to_latest_user_chat_message`` (OpenAI helper), which
|
||||
scans backwards through all messages without frozen-count awareness.
|
||||
- Does NOT skip injection in cache mode (the OpenAI handler injects in all
|
||||
modes — there is no is_cache_mode gate around memory).
|
||||
- The engine uses ``_on_request_openai_chat`` via ``OpenAIComponents``.
|
||||
- prefetched_memory_context is supplied via RequestContext (4.3-i pattern).
|
||||
|
||||
Running
|
||||
-------
|
||||
.venv/bin/python -m pytest tests/engine/test_facade_openai_memory.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FIXED_CONTEXT = (
|
||||
"## Relevant memory\n- auth_middleware.py: JWT validation\n- auth_router.py: /login route"
|
||||
)
|
||||
|
||||
|
||||
def _make_engine(
|
||||
*,
|
||||
memory_handler: Any | None = None,
|
||||
with_memory: bool = True,
|
||||
config_overrides: dict[str, Any] | None = None,
|
||||
frozen_count: int = 0,
|
||||
) -> Any:
|
||||
"""Build a HeadroomEngine with OpenAIComponents + MemoryComponents."""
|
||||
from headroom.engine.contract import Flavor, Provider
|
||||
from headroom.engine.facade import HeadroomEngine, MemoryComponents, OpenAIComponents
|
||||
from headroom.proxy.models import ProxyConfig
|
||||
from headroom.proxy.server import HeadroomProxy
|
||||
|
||||
config_kwargs: dict[str, Any] = {
|
||||
"optimize": True,
|
||||
"mode": "token",
|
||||
"cache_enabled": False,
|
||||
"rate_limit_enabled": False,
|
||||
"cost_tracking_enabled": False,
|
||||
"log_requests": False,
|
||||
"ccr_inject_tool": False,
|
||||
"ccr_inject_system_instructions": False,
|
||||
"ccr_handle_responses": False,
|
||||
"ccr_context_tracking": False,
|
||||
"ccr_proactive_expansion": False,
|
||||
"image_optimize": False,
|
||||
}
|
||||
if config_overrides:
|
||||
config_kwargs.update(config_overrides)
|
||||
|
||||
config = ProxyConfig(**config_kwargs)
|
||||
proxy = HeadroomProxy(config)
|
||||
|
||||
class _FixedStore:
|
||||
def compute_session_id(self, ctx: Any, model: str, msgs: Any) -> str:
|
||||
return "memory-openai-structural-test-session"
|
||||
|
||||
def get_or_create(self, session_id: str, provider: str) -> Any:
|
||||
class _T:
|
||||
def get_frozen_message_count(self) -> int:
|
||||
return frozen_count
|
||||
|
||||
def get_last_original_messages(self) -> list[Any]:
|
||||
return []
|
||||
|
||||
def get_last_forwarded_messages(self) -> list[Any]:
|
||||
return []
|
||||
|
||||
return _T()
|
||||
|
||||
def get_fresh_cache(self, session_id: str) -> Any:
|
||||
class _C:
|
||||
def apply_cached(self, msgs: list[Any]) -> list[Any]:
|
||||
return list(msgs)
|
||||
|
||||
def compute_frozen_count(self, msgs: list[Any]) -> int:
|
||||
return 0
|
||||
|
||||
def update_from_result(self, orig: Any, compr: Any) -> None:
|
||||
pass
|
||||
|
||||
def mark_stable_from_messages(self, msgs: Any, up_to: int) -> None:
|
||||
pass
|
||||
|
||||
return _C()
|
||||
|
||||
oc = OpenAIComponents(
|
||||
pipeline=proxy.openai_pipeline,
|
||||
provider=proxy.openai_provider,
|
||||
session_tracker_store=_FixedStore(),
|
||||
get_compression_cache=_FixedStore().get_fresh_cache,
|
||||
config=proxy.config,
|
||||
usage_reporter=None,
|
||||
)
|
||||
|
||||
mc = None
|
||||
if with_memory:
|
||||
_handler = memory_handler
|
||||
if _handler is None:
|
||||
_handler = MagicMock()
|
||||
_handler.config.inject_context = True
|
||||
|
||||
mc = MemoryComponents(
|
||||
memory_handler=_handler,
|
||||
default_user_id="test-user",
|
||||
)
|
||||
|
||||
engine = HeadroomEngine(
|
||||
pipelines={(Provider.OPENAI, Flavor.CHAT): proxy.openai_pipeline},
|
||||
config=proxy.config,
|
||||
usage_reporter=None,
|
||||
salt=b"memory-openai-structural-test-salt",
|
||||
openai_components=oc,
|
||||
memory_components=mc,
|
||||
)
|
||||
return engine
|
||||
|
||||
|
||||
def _make_ctx(
|
||||
body: dict[str, Any],
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
prefetched_memory_context: str | None = None,
|
||||
) -> Any:
|
||||
"""Build a RequestContext for OpenAI structural tests."""
|
||||
from headroom.engine.contract import Flavor, Provider, RequestContext
|
||||
|
||||
h: dict[str, str] = {
|
||||
"authorization": "Bearer sk-test-openai-key",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
|
||||
return RequestContext(
|
||||
provider=Provider.OPENAI,
|
||||
flavor=Flavor.CHAT,
|
||||
headers_view=h,
|
||||
raw_body=json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode(),
|
||||
session_key="memory-openai-structural",
|
||||
request_id="req-openai-mem-test",
|
||||
prefetched_memory_context=prefetched_memory_context,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Memory off → no-op (MemoryComponents=None)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_noop_when_components_none_openai() -> None:
|
||||
"""Engine without MemoryComponents is byte-identical — OpenAI golden fixtures unaffected."""
|
||||
engine = _make_engine(with_memory=False)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
assert _FIXED_CONTEXT not in out["messages"][-1]["content"], (
|
||||
"Memory context must not appear when MemoryComponents is None"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Byte-exact placement: string content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_string_content_openai() -> None:
|
||||
"""Memory-on: context appended to latest user message (string content).
|
||||
|
||||
Placement rule: ``original_text + "\\n\\n" + context`` — same as
|
||||
``append_text_to_latest_user_chat_message`` in the handler.
|
||||
"""
|
||||
user_text = "How does authentication work?"
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": user_text}],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
engine = _make_engine()
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
last_msg = out["messages"][-1]
|
||||
assert last_msg["role"] == "user"
|
||||
content = last_msg["content"]
|
||||
assert isinstance(content, str)
|
||||
assert content == user_text + "\n\n" + _FIXED_CONTEXT
|
||||
|
||||
|
||||
def test_memory_injection_list_content_openai() -> None:
|
||||
"""Memory-on: context appended to first text block (list content)."""
|
||||
original_text = "What is the rate limit?"
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": original_text},
|
||||
{"type": "text", "text": "Additional detail."},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
engine = _make_engine()
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
last_msg = out["messages"][-1]
|
||||
blocks = last_msg["content"]
|
||||
assert isinstance(blocks, list)
|
||||
# First text block gets the injection.
|
||||
assert blocks[0]["text"] == original_text + "\n\n" + _FIXED_CONTEXT
|
||||
# Second text block unchanged.
|
||||
assert blocks[1]["text"] == "Additional detail."
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Cache mode: memory injection NOT skipped (OpenAI differs from Anthropic)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_not_skipped_in_cache_mode_openai() -> None:
|
||||
"""OpenAI: memory injection is NOT skipped in cache mode.
|
||||
|
||||
The live ``handle_openai_chat`` handler has no is_cache_mode gate around
|
||||
the memory injection block. The Anthropic engine skips injection in cache
|
||||
mode; the OpenAI engine must NOT.
|
||||
"""
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "Cache mode test"}],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
engine = _make_engine(config_overrides={"mode": "cache"})
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
last_msg = out["messages"][-1]
|
||||
content = last_msg.get("content", "")
|
||||
assert _FIXED_CONTEXT in content, "OpenAI memory injection must NOT be skipped in cache mode"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Memory injection scans backwards (frozen-count agnostic)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_scans_backwards_to_latest_user_openai() -> None:
|
||||
"""Memory is injected into the latest user message regardless of frozen count.
|
||||
|
||||
``append_text_to_latest_user_chat_message`` scans backwards from the end
|
||||
of the message array without frozen-count awareness — the OpenAI handler
|
||||
does not pass a frozen count to the helper.
|
||||
"""
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Turn 1 (earlier)"},
|
||||
{"role": "assistant", "content": "Turn 1 answer"},
|
||||
{"role": "user", "content": "Turn 2 (latest)"},
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
# frozen_count=2 — but the helper ignores it; latest user turn still receives context.
|
||||
engine = _make_engine(frozen_count=2)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
msgs = out["messages"]
|
||||
# Latest (last) user turn must have context injected.
|
||||
assert msgs[-1]["role"] == "user"
|
||||
assert _FIXED_CONTEXT in msgs[-1]["content"]
|
||||
# Earlier user turn must be unchanged.
|
||||
assert _FIXED_CONTEXT not in msgs[0]["content"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Empty / None prefetched context → no-op
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_skipped_on_empty_context_openai() -> None:
|
||||
"""Empty prefetched_memory_context → no injection."""
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context="")
|
||||
engine = _make_engine()
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
assert out["messages"][-1]["content"] == "Hello"
|
||||
|
||||
|
||||
def test_memory_injection_skipped_on_none_context_openai() -> None:
|
||||
"""None prefetched_memory_context → no injection."""
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=None)
|
||||
engine = _make_engine()
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
assert out["messages"][-1]["content"] == "Hello"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. inject_context=False gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_skipped_when_inject_context_false_openai() -> None:
|
||||
"""memory_handler.config.inject_context=False → injection skipped."""
|
||||
handler_mock = MagicMock()
|
||||
handler_mock.config.inject_context = False
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "No inject_context test"}],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
engine = _make_engine(memory_handler=handler_mock)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
assert _FIXED_CONTEXT not in out["messages"][-1]["content"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. Bypass gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_skipped_on_bypass_openai() -> None:
|
||||
"""x-headroom-bypass: true → memory injection is skipped."""
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "Bypass test"}],
|
||||
}
|
||||
ctx = _make_ctx(
|
||||
body,
|
||||
headers={"x-headroom-bypass": "true"},
|
||||
prefetched_memory_context=_FIXED_CONTEXT,
|
||||
)
|
||||
engine = _make_engine()
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
assert _FIXED_CONTEXT.encode() not in decision.body, (
|
||||
"Memory context must not appear in body under bypass"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. No user_id → MemoryDecision gate fails
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_skipped_when_no_user_id_openai() -> None:
|
||||
"""No user_id → MemoryDecision.inject=False → no injection.
|
||||
|
||||
Supply MemoryComponents with default_user_id="" and no x-headroom-user-id
|
||||
header. MemoryDecision.decide gates on memory_user_id being non-empty.
|
||||
"""
|
||||
from headroom.engine.contract import Flavor, Provider
|
||||
from headroom.engine.facade import HeadroomEngine, MemoryComponents, OpenAIComponents
|
||||
from headroom.proxy.models import ProxyConfig
|
||||
from headroom.proxy.server import HeadroomProxy
|
||||
|
||||
config = ProxyConfig(
|
||||
optimize=True,
|
||||
mode="token",
|
||||
cache_enabled=False,
|
||||
rate_limit_enabled=False,
|
||||
cost_tracking_enabled=False,
|
||||
log_requests=False,
|
||||
image_optimize=False,
|
||||
)
|
||||
proxy = HeadroomProxy(config)
|
||||
|
||||
class _S:
|
||||
def compute_session_id(self, *a: Any, **kw: Any) -> str:
|
||||
return "s"
|
||||
|
||||
def get_or_create(self, *a: Any, **kw: Any) -> Any:
|
||||
class _T:
|
||||
def get_frozen_message_count(self) -> int:
|
||||
return 0
|
||||
|
||||
def get_last_original_messages(self) -> list:
|
||||
return []
|
||||
|
||||
def get_last_forwarded_messages(self) -> list:
|
||||
return []
|
||||
|
||||
return _T()
|
||||
|
||||
def get_fresh_cache(self, sid: str) -> Any:
|
||||
class _C:
|
||||
def apply_cached(self, m: list) -> list:
|
||||
return list(m)
|
||||
|
||||
def compute_frozen_count(self, m: list) -> int:
|
||||
return 0
|
||||
|
||||
def update_from_result(self, *a: Any) -> None:
|
||||
pass
|
||||
|
||||
def mark_stable_from_messages(self, *a: Any) -> None:
|
||||
pass
|
||||
|
||||
return _C()
|
||||
|
||||
handler_mock = MagicMock()
|
||||
handler_mock.config.inject_context = True
|
||||
|
||||
oc = OpenAIComponents(
|
||||
pipeline=proxy.openai_pipeline,
|
||||
provider=proxy.openai_provider,
|
||||
session_tracker_store=_S(),
|
||||
get_compression_cache=_S().get_fresh_cache,
|
||||
config=proxy.config,
|
||||
usage_reporter=None,
|
||||
)
|
||||
mc = MemoryComponents(
|
||||
memory_handler=handler_mock,
|
||||
default_user_id="", # empty → gate fails
|
||||
)
|
||||
engine = HeadroomEngine(
|
||||
pipelines={(Provider.OPENAI, Flavor.CHAT): proxy.openai_pipeline},
|
||||
config=proxy.config,
|
||||
usage_reporter=None,
|
||||
salt=b"s",
|
||||
openai_components=oc,
|
||||
memory_components=mc,
|
||||
)
|
||||
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "No user_id test"}],
|
||||
}
|
||||
# No x-headroom-user-id header and default_user_id="" → gate fails.
|
||||
from headroom.engine.contract import RequestContext
|
||||
|
||||
ctx = RequestContext(
|
||||
provider=Provider.OPENAI,
|
||||
flavor=Flavor.CHAT,
|
||||
headers_view={"authorization": "Bearer sk-test"},
|
||||
raw_body=json.dumps(body, separators=(",", ":")).encode(),
|
||||
session_key="s",
|
||||
request_id="",
|
||||
prefetched_memory_context=_FIXED_CONTEXT,
|
||||
)
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
assert _FIXED_CONTEXT not in out["messages"][-1]["content"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 9. Multi-turn body: context lands in latest user message (not assistant)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_memory_injection_skips_assistant_tail_openai() -> None:
|
||||
"""Memory injection finds the last user message even when last message is assistant."""
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Turn 1"},
|
||||
{"role": "assistant", "content": "Ack"},
|
||||
],
|
||||
}
|
||||
ctx = _make_ctx(body, prefetched_memory_context=_FIXED_CONTEXT)
|
||||
engine = _make_engine()
|
||||
decision = engine.on_request(ctx)
|
||||
|
||||
out = json.loads(decision.body)
|
||||
msgs = out["messages"]
|
||||
# Last message is assistant — the helper scans back and finds the user turn.
|
||||
assert msgs[-2]["role"] == "user"
|
||||
assert _FIXED_CONTEXT in msgs[-2]["content"]
|
||||
# Assistant message is unchanged.
|
||||
assert _FIXED_CONTEXT not in msgs[-1]["content"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue