Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,16 +1058,29 @@ def create(self, **kwargs) -> Any:
# 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,
_default_prompt_cache_retention_for_request,
)
from utils import base_url_host_matches

_host_src = str(getattr(self._client, "base_url", "") or "")
_is_xai = base_url_host_matches(_host_src, "x.ai") or base_url_host_matches(_host_src, "api.x.ai")
_is_github = base_url_host_matches(_host_src, "githubcopilot.com")
_is_github = (
base_url_host_matches(_host_src, "githubcopilot.com")
or base_url_host_matches(_host_src, "models.github.ai")
)
if not _is_xai and not _is_github and "prompt_cache_key" not in resp_kwargs:
_cache_key = _content_cache_key(instructions, resp_kwargs.get("tools"))
if _cache_key:
resp_kwargs["prompt_cache_key"] = _cache_key
if "prompt_cache_retention" not in resp_kwargs:
_cache_retention = _default_prompt_cache_retention_for_request(
model,
_host_src,
)
if _cache_retention:
resp_kwargs["prompt_cache_retention"] = _cache_retention
except Exception:
logger.debug(
"Codex auxiliary: prompt_cache_key derivation skipped", exc_info=True
Expand Down
1 change: 1 addition & 0 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
tools=tools_for_api,
reasoning_config=agent.reasoning_config,
session_id=getattr(agent, "session_id", None),
base_url=agent.base_url,
max_tokens=agent.max_tokens,
timeout=agent._resolved_api_call_timeout(),
request_overrides=agent.request_overrides,
Expand Down
12 changes: 9 additions & 3 deletions agent/codex_responses_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions agent/transports/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import hashlib
import json
import re
from typing import Any, Dict, List, Optional

from agent.transports.base import ProviderTransport
Expand All @@ -27,6 +28,49 @@ def _bounded_prompt_cache_key(value: Any) -> Optional[str]:
return f"pck_{digest}"


_EXTENDED_PROMPT_CACHE_MODELS = (
"gpt-5.5-pro",
"gpt-5.5",
"gpt-5.4",
"gpt-5.2",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5.1-chat-latest",
"gpt-5.1-codex",
"gpt-5.1",
"gpt-5-codex",
"gpt-5",
"gpt-4.1",
)
_EXTENDED_PROMPT_CACHE_MODEL_RE = re.compile(
rf"(?:^|[./:])(?:{'|'.join(re.escape(name) for name in _EXTENDED_PROMPT_CACHE_MODELS)})"
r"(?:-\d{4}-\d{2}-\d{2})?$"
)


def _default_prompt_cache_retention_for_request(
model: str,
base_url: Any,
) -> Optional[str]:
"""Return ``24h`` for supported models on Amazon Bedrock Mantle."""
from utils import base_url_hostname

hostname_parts = base_url_hostname(str(base_url or "")).split(".")
is_bedrock_mantle = (
len(hostname_parts) == 4
and hostname_parts[0] == "bedrock-mantle"
and bool(hostname_parts[1])
and hostname_parts[2:] == ["api", "aws"]
)
if not is_bedrock_mantle:
return None

normalized = str(model or "").strip().lower().replace("_", "-")
if _EXTENDED_PROMPT_CACHE_MODEL_RE.search(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.

Expand Down Expand Up @@ -284,6 +328,13 @@ def build_kwargs(
if not is_github_responses and not is_xai_responses and cache_key:
kwargs["prompt_cache_key"] = cache_key

cache_retention = _default_prompt_cache_retention_for_request(
model,
params.get("base_url"),
)
if cache_retention:
kwargs.setdefault("prompt_cache_retention", cache_retention)

if reasoning_enabled and is_xai_responses:
from agent.model_metadata import grok_supports_reasoning_effort

Expand Down
67 changes: 65 additions & 2 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4327,7 +4327,7 @@ class TestCodexAdapterPromptCacheKey:
"""

@staticmethod
def _build_adapter(base_url="https://chatgpt.com/backend-api/codex"):
def _build_adapter(base_url="https://chatgpt.com/backend-api/codex", model="gpt-5.5"):
from agent.auxiliary_client import _CodexCompletionsAdapter
from types import SimpleNamespace

Expand Down Expand Up @@ -4360,7 +4360,7 @@ def _create(**kwargs):
real_client = MagicMock()
real_client.base_url = base_url
real_client.responses.create = _create
adapter = _CodexCompletionsAdapter(real_client, "gpt-5.5")
adapter = _CodexCompletionsAdapter(real_client, model)
return adapter, captured_kwargs

def test_cache_key_set_and_prefixed(self):
Expand Down Expand Up @@ -4413,6 +4413,69 @@ def test_cache_key_skipped_for_github_copilot_host(self):
])
assert "prompt_cache_key" not in captured

@pytest.mark.parametrize("model", [
"gpt-4.1",
"gpt-5.1-codex-max",
"openai.gpt-5.5-pro",
])
def test_extended_cache_models_set_prompt_cache_retention(self, model):
adapter, captured = self._build_adapter(
base_url="https://bedrock-mantle.us-west-2.api.aws/v1",
model=model,
)
adapter.create(messages=[
{"role": "system", "content": "SYS"},
{"role": "user", "content": "hi"},
])
assert captured["prompt_cache_retention"] == "24h"

def test_prompt_cache_retention_skipped_for_codex_backend(self):
adapter, captured = self._build_adapter()
adapter.create(messages=[
{"role": "system", "content": "SYS"},
{"role": "user", "content": "hi"},
])
assert "prompt_cache_retention" not in captured

@pytest.mark.parametrize("base_url", [
"https://api.openai.com/v1",
"https://example.services.ai.azure.com/openai/v1",
"https://responses.example.com/v1",
])
def test_prompt_cache_retention_skipped_for_other_compatible_endpoints(self, base_url):
adapter, captured = self._build_adapter(base_url=base_url)
adapter.create(messages=[
{"role": "system", "content": "SYS"},
{"role": "user", "content": "hi"},
])
assert "prompt_cache_retention" not in captured

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

def test_prompt_cache_retention_skipped_for_github_models_host(self):
"""models.github.ai is a GitHub Responses host in the main transport
(agent/chat_completion_helpers.py) — the auxiliary path must exclude
it from cache-retention emission the same way as githubcopilot.com."""
adapter, captured = self._build_adapter(base_url="https://models.github.ai/inference")
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
Expand Down
56 changes: 56 additions & 0 deletions tests/agent/transports/test_codex_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,62 @@ def test_non_github_responses_keeps_message_item_id_end_to_end(self, transport):
message_item = next(item for item in kw["input"] if item.get("type") == "message")
assert message_item["id"] == "msg_short_id"

@pytest.mark.parametrize("model", [
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.2",
"gpt-5.1-codex-max",
"gpt-5.1",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1-chat-latest",
"gpt-5",
"gpt-5-codex",
"gpt-4.1",
"openai.gpt-5.5-pro",
"openai/gpt-5.1-codex-2026-01-01",
])
def test_extended_cache_models_set_24h_prompt_cache_retention(self, transport, model):
messages = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs(
model=model, messages=messages, tools=[],
session_id="test-session",
base_url="https://bedrock-mantle.us-west-2.api.aws/v1",
)
assert kw["prompt_cache_retention"] == "24h"

@pytest.mark.parametrize("model", ["gpt-5.6", "gpt-4o", "o3"])
def test_prompt_cache_retention_omitted_for_other_model_families(self, transport, model):
kw = transport.build_kwargs(
model=model,
messages=[{"role": "user", "content": "Hi"}],
tools=[],
session_id="test-session",
base_url="https://bedrock-mantle.us-west-2.api.aws/v1",
)
assert "prompt_cache_retention" not in kw

@pytest.mark.parametrize("base_url", [
"https://api.openai.com/v1",
"https://example.openai.azure.com/openai/v1",
"https://api.x.ai/v1",
"https://models.github.ai/inference",
"https://api.githubcopilot.com",
"https://chatgpt.com/backend-api/codex",
"https://responses.example.com/v1",
"https://bedrock-mantle.us-west-2.api.aws.example/v1",
"https://example.com/bedrock-mantle.us-west-2.api.aws/v1",
])
def test_prompt_cache_retention_omitted_for_non_mantle_endpoints(self, transport, base_url):
kw = transport.build_kwargs(
model="gpt-5.4",
messages=[{"role": "user", "content": "Hi"}],
tools=[],
base_url=base_url,
)
assert "prompt_cache_retention" not in kw

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
Expand Down
19 changes: 19 additions & 0 deletions tests/run_agent/test_run_agent_codex_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,25 @@ def test_build_api_kwargs_codex(monkeypatch):
assert "extra_body" not in kwargs


def test_build_api_kwargs_mantle_sets_extended_prompt_cache_retention(monkeypatch):
_patch_agent_bootstrap(monkeypatch)
agent = run_agent.AIAgent(
model="openai.gpt-5.5",
provider="custom",
api_mode="codex_responses",
base_url="https://bedrock-mantle.us-west-2.api.aws/v1",
api_key="test-token",
quiet_mode=True,
max_iterations=1,
skip_context_files=True,
skip_memory=True,
)

kwargs = agent._build_api_kwargs([{"role": "user", "content": "Ping"}])

assert kwargs["prompt_cache_retention"] == "24h"


def test_build_api_kwargs_codex_clamps_minimal_effort(monkeypatch):
"""'minimal' reasoning effort is clamped to 'low' on the Responses API.

Expand Down
Loading