mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
fix(proxy): preserve Responses memory continuations with store=false (#1103)
## Description Previously, Responses API memory tools could execute successfully but fail on the follow-up request when the client sent `store=false`. Headroom sends memory tool results back with `previous_response_id`, but upstream cannot continue from a response that was not stored. This PR forces `store=true` only when Headroom actually injects Responses memory tools, keeping ordinary `store=false` requests unchanged. ## Type of Change - [x] 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 - [ ] Code refactoring (no functional changes) ## Changes Made - Added `_ensure_responses_store_for_memory_tools` to make the Responses memory-tool continuation precondition explicit. - Call it only after Responses memory tools are injected. - Added regression coverage for `store=false`, plus no-op coverage for unrelated requests. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ /opt/homebrew/bin/uv run --extra dev pytest tests/test_openai_responses_context_compaction.py -q bind: Invalid command `vi-cmd-mode`. bind: Invalid command `vi-cmd-mode`. ============================= test session starts ============================== platform darwin -- Python 3.12.11, pytest-9.0.3, pluggy-1.6.0 rootdir: /Users/ianks/src/github.com/chopratejas/headroom configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.8.0, asyncio-1.3.0 asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 11 items tests/test_openai_responses_context_compaction.py ........... [100%] ======================= 11 passed, 14 warnings in 4.27s ======================== $ /opt/homebrew/bin/uv run --extra dev ruff check headroom/proxy/handlers/openai.py tests/test_openai_responses_context_compaction.py All checks passed! $ git diff --check $ /opt/homebrew/bin/uv run --extra dev mypy headroom headroom/proxy/server.py:1151: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] headroom/proxy/server.py:1221: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] headroom/proxy/server.py:1225: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] Success: no issues found in 374 source files ``` ## Real Behavior Proof - Environment: macOS, `headroom-ai` 0.25.0 local proxy, OpenAI Responses traffic through `http://127.0.0.1:8787/v1` to `https://proxy.shopify.ai`. - Exact command / steps: sent a Responses request with `store=false` asking the model to save `HEADROOM_MEMORY_TEST_MARKER_1781746500`, then sent another `store=false` Responses request asking the model to recall it via memory search. - Observed result: before the local patch, `memory_save` persisted SQLite but continuation failed with `previous_response_not_found`; after the local patch, the same recall path returned `200` and replied `HEADROOM_MEMORY_TEST_MARKER_1781746500 means Headroom memory tools tested pi.` - Not tested: full upstream integration test against the real OpenAI API in CI; this PR covers the payload precondition with unit tests and local proxy manual verification. ## 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 - [x] I have updated CHANGELOG.md if applicable ## Additional Notes Changelog updated. No docs update; this is a small proxy bug fix. Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
This commit is contained in:
parent
b4fde0c3a4
commit
cdfeeacc63
3 changed files with 80 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* **proxy:** force Responses API `store=true` when Headroom injects memory tools so `previous_response_id` continuations work after memory tool calls from clients that requested `store=false` ([#1103](https://github.com/chopratejas/headroom/pull/1103)).
|
||||
* **proxy:** build SSL contexts for custom CA bundles so enterprise/private PKI roots work with Python/OpenSSL strict verification.
|
||||
* **proxy:** route Codex OAuth image generation and edit requests through the ChatGPT Codex image backend, while preserving OpenAI API-key image passthrough ([#1215](https://github.com/chopratejas/headroom/pull/1215)).
|
||||
* **wrap (codex):** keep RTK guidance in the global Codex `AGENTS.md` instead of modifying the shared project `AGENTS.md` ([#1235](https://github.com/chopratejas/headroom/issues/1235)).
|
||||
|
|
|
|||
|
|
@ -321,6 +321,29 @@ def _compact_openai_responses_tools(
|
|||
return updated, True, before, after
|
||||
|
||||
|
||||
def _ensure_responses_store_for_memory_tools(
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
memory_tools_injected: bool,
|
||||
) -> bool:
|
||||
"""Keep Responses API memory-tool continuations addressable.
|
||||
|
||||
Memory tools are transparent to clients: Headroom executes the emitted
|
||||
function_call, then sends function_call_output in a continuation request
|
||||
using previous_response_id. OpenAI only allows that continuation when the
|
||||
previous response was stored. Clients such as pi/Codex can set store=false
|
||||
to avoid retaining ordinary responses, but that makes memory-tool
|
||||
continuations fail with previous_response_not_found.
|
||||
|
||||
Return True when this function changes the payload.
|
||||
"""
|
||||
|
||||
if memory_tools_injected and payload.get("store") is False:
|
||||
payload["store"] = True
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _responses_input_item_text_bytes(item: Any) -> int:
|
||||
if not isinstance(item, dict):
|
||||
return _json_byte_len(item)
|
||||
|
|
@ -3093,6 +3116,14 @@ class OpenAIHandlerMixin:
|
|||
if mem_tools_injected:
|
||||
body["tools"] = resp_tools
|
||||
logger.info(f"[{request_id}] Memory: Injected memory tools (openai/responses)")
|
||||
|
||||
if _ensure_responses_store_for_memory_tools(
|
||||
body,
|
||||
memory_tools_injected=True,
|
||||
):
|
||||
logger.info(
|
||||
f"[{request_id}] Memory: forced store=true for Responses memory tool continuation"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{request_id}] Memory injection failed (responses): {e}")
|
||||
elif self.memory_handler and memory_user_id and _bypass:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import Any
|
|||
from headroom.proxy.handlers.openai import (
|
||||
OpenAIHandlerMixin,
|
||||
_compact_openai_responses_tools,
|
||||
_ensure_responses_store_for_memory_tools,
|
||||
_openai_responses_context_budget,
|
||||
)
|
||||
from headroom.transforms.content_router import (
|
||||
|
|
@ -435,3 +436,50 @@ def test_content_router_retries_kompress_when_structured_strategy_noops(monkeypa
|
|||
assert compressed_tokens == 2
|
||||
# The fallback chain must record both strategies it tried.
|
||||
assert strategy_chain == ["smart_crusher", "kompress"]
|
||||
|
||||
|
||||
def test_responses_memory_tools_store_false_regression() -> None:
|
||||
"""Regression: store=false makes previous_response_id continuations fail."""
|
||||
|
||||
payload = {"model": "gpt-5.5", "input": "remember this", "store": False}
|
||||
|
||||
changed = _ensure_responses_store_for_memory_tools(
|
||||
payload,
|
||||
memory_tools_injected=True,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
assert payload["store"] is True
|
||||
|
||||
|
||||
def test_responses_memory_tools_do_not_change_unrelated_requests() -> None:
|
||||
no_memory_payload = {"model": "gpt-5.5", "input": "plain", "store": False}
|
||||
already_stored_payload = {"model": "gpt-5.5", "input": "plain", "store": True}
|
||||
default_store_payload = {"model": "gpt-5.5", "input": "plain"}
|
||||
|
||||
assert (
|
||||
_ensure_responses_store_for_memory_tools(
|
||||
no_memory_payload,
|
||||
memory_tools_injected=False,
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert no_memory_payload["store"] is False
|
||||
|
||||
assert (
|
||||
_ensure_responses_store_for_memory_tools(
|
||||
already_stored_payload,
|
||||
memory_tools_injected=True,
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert already_stored_payload["store"] is True
|
||||
|
||||
assert (
|
||||
_ensure_responses_store_for_memory_tools(
|
||||
default_store_payload,
|
||||
memory_tools_injected=True,
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert "store" not in default_store_payload
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue