diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index dc5c2648f7a0..a84ba93a1bca 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -1058,7 +1058,10 @@ class _CodexCompletionsAdapter: # key in extra_body (not top-level) and GitHub/Copilot Responses opts # out of cache-key routing entirely — for those hosts, skip it here. try: - from agent.transports.codex import _content_cache_key + from agent.transports.codex import ( + _content_cache_key, + _prompt_cache_retention_for_model, + ) from utils import base_url_host_matches _host_src = str(getattr(self._client, "base_url", "") or "") @@ -1068,6 +1071,10 @@ class _CodexCompletionsAdapter: _cache_key = _content_cache_key(instructions, resp_kwargs.get("tools")) if _cache_key: resp_kwargs["prompt_cache_key"] = _cache_key + if not _is_xai and not _is_github and "prompt_cache_retention" not in resp_kwargs: + _cache_retention = _prompt_cache_retention_for_model(model) + if _cache_retention: + resp_kwargs["prompt_cache_retention"] = _cache_retention except Exception: logger.debug( "Codex auxiliary: prompt_cache_key derivation skipped", exc_info=True diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index bce372ebb5da..ee75f4190e6d 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -912,7 +912,8 @@ def _preflight_codex_api_kwargs( allowed_keys = { "model", "instructions", "input", "tools", "store", "reasoning", "include", "max_output_tokens", "temperature", - "tool_choice", "parallel_tool_calls", "prompt_cache_key", "service_tier", + "tool_choice", "parallel_tool_calls", "prompt_cache_key", + "prompt_cache_retention", "service_tier", "extra_headers", "extra_body", "timeout", } normalized: Dict[str, Any] = { @@ -950,8 +951,13 @@ def _preflight_codex_api_kwargs( if isinstance(temperature, (int, float)): normalized["temperature"] = float(temperature) - # Pass through tool_choice, parallel_tool_calls, prompt_cache_key - for passthrough_key in ("tool_choice", "parallel_tool_calls", "prompt_cache_key"): + # Pass through cache routing/retention and tool-dispatch hints. + for passthrough_key in ( + "tool_choice", + "parallel_tool_calls", + "prompt_cache_key", + "prompt_cache_retention", + ): val = api_kwargs.get(passthrough_key) if val is not None: normalized[passthrough_key] = val diff --git a/agent/transports/codex.py b/agent/transports/codex.py index 5855fcfe9c54..f6bfda2cde4f 100644 --- a/agent/transports/codex.py +++ b/agent/transports/codex.py @@ -27,6 +27,23 @@ def _bounded_prompt_cache_key(value: Any) -> Optional[str]: return f"pck_{digest}" +def _prompt_cache_retention_for_model(model: str) -> Optional[str]: + """Return the OpenAI Responses prompt-cache retention policy for models + that require an explicit policy. + + OpenAI documents GPT-5.5 / GPT-5.5 Pro as extended-cache-only + (``prompt_cache_retention: "24h"``). Sending the field only for + those model families keeps older/OpenAI-compatible relays on their + default behavior. + """ + normalized = str(model or "").lower().replace("_", "-") + # Custom relays commonly prefix provider namespaces, e.g. + # ``openai.gpt-5.5``. Match both bare and namespaced model ids. + if normalized.endswith("gpt-5.5") or "gpt-5.5-" in normalized: + return "24h" + return None + + def _content_cache_key(instructions: str, tools: Optional[List[Dict[str, Any]]]) -> Optional[str]: """Content-address the prompt cache key from the static request prefix. @@ -284,6 +301,15 @@ class ResponsesApiTransport(ProviderTransport): if not is_github_responses and not is_xai_responses and cache_key: kwargs["prompt_cache_key"] = cache_key + cache_retention = _prompt_cache_retention_for_model(model) + if ( + cache_retention + and not is_github_responses + and not is_xai_responses + and not is_codex_backend + ): + kwargs.setdefault("prompt_cache_retention", cache_retention) + if reasoning_enabled and is_xai_responses: from agent.model_metadata import grok_supports_reasoning_effort diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index 921f84ae99d9..3058f2e9c3c3 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -4958,6 +4958,29 @@ class TestCodexAdapterPromptCacheKey: ]) assert "prompt_cache_key" not in captured + def test_gpt55_sets_prompt_cache_retention(self): + adapter, captured = self._build_adapter(base_url="https://bedrock-mantle.us-east-1.api.aws/openai/v1") + adapter.create(messages=[ + {"role": "system", "content": "SYS"}, + {"role": "user", "content": "hi"}, + ]) + assert captured["prompt_cache_retention"] == "24h" + + def test_prompt_cache_retention_skipped_for_xai_and_github_hosts(self): + adapter, captured = self._build_adapter(base_url="https://api.x.ai/v1") + adapter.create(messages=[ + {"role": "system", "content": "SYS"}, + {"role": "user", "content": "hi"}, + ]) + assert "prompt_cache_retention" not in captured + + adapter, captured = self._build_adapter(base_url="https://api.githubcopilot.com") + adapter.create(messages=[ + {"role": "system", "content": "SYS"}, + {"role": "user", "content": "hi"}, + ]) + assert "prompt_cache_retention" not in captured + class TestCodexAdapterGithubResponsesMessageIdDrop: """_CodexCompletionsAdapter must drop codex_message_items ``id`` when diff --git a/tests/agent/transports/test_codex_transport.py b/tests/agent/transports/test_codex_transport.py index 1a17a39a7744..56805ae22777 100644 --- a/tests/agent/transports/test_codex_transport.py +++ b/tests/agent/transports/test_codex_transport.py @@ -243,6 +243,26 @@ class TestCodexBuildKwargs: message_item = next(item for item in kw["input"] if item.get("type") == "message") assert message_item["id"] == "msg_short_id" + def test_gpt55_sets_prompt_cache_retention(self, transport): + messages = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="openai.gpt-5.5", messages=messages, tools=[], + session_id="test-session", + ) + assert kw["prompt_cache_retention"] == "24h" + + def test_gpt55_prompt_cache_retention_skipped_for_known_incompatible_backends(self, transport): + messages = [{"role": "user", "content": "Hi"}] + assert "prompt_cache_retention" not in transport.build_kwargs( + model="gpt-5.5", messages=messages, tools=[], is_xai_responses=True + ) + assert "prompt_cache_retention" not in transport.build_kwargs( + model="gpt-5.5", messages=messages, tools=[], is_github_responses=True + ) + assert "prompt_cache_retention" not in transport.build_kwargs( + model="gpt-5.5", messages=messages, tools=[], is_codex_backend=True + ) + def test_xai_responses_sends_cache_key_via_extra_body(self, transport): """xAI's Responses API documents ``prompt_cache_key`` as the body-level cache-routing key (the ``x-grok-conv-id`` header is