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
8 changes: 7 additions & 1 deletion agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1402,7 +1402,13 @@ def _build_responses_kwargs(self, kwargs: Dict[str, Any]) -> Tuple[Dict[str, Any
input_items = _chat_messages_to_responses_input(
replay_messages, is_github_responses=is_copilot,
current_issuer_kind=_classify_responses_issuer(base_url=host, **route._asdict()),
current_issuer_model=wire_model, native_compaction_eligible=False,
current_issuer_model=wire_model,
native_compaction_eligible=False,
is_azure_foundry=(
str(_runtime_main_value("provider") or "").strip().lower() == "azure-foundry"
or base_url_host_matches(host, "services.ai.azure.com")
or base_url_host_matches(host, "openai.azure.com")
),
)
resp_kwargs: Dict[str, Any] = {
# Codex only knows the base slug; strip the Hermes ``-900k`` picker suffix.
Expand Down
59 changes: 48 additions & 11 deletions agent/codex_responses_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,21 @@ def _assistant_message_item(
)


def _apply_azure_output_text_annotations(parts: List[Dict[str, Any]]) -> None:
"""Add Azure's required annotations field to output_text parts only.

Azure validates replayed assistant output blocks more strictly than the
generic Responses endpoint. Never add this field to input_image parts.
"""
for part in parts:
if isinstance(part, dict) and part.get("type") == "output_text":
part.setdefault("annotations", [])


def _replay_reasoning_items(
msg: Dict[str, Any], *, seen_item_ids: set, current_issuer_kind: Optional[str],
current_issuer_model: Optional[str] = None, native_compaction_eligible: bool,
is_azure_foundry: bool = False,
) -> List[Dict[str, Any]]:
"""Replay persisted encrypted reasoning/compaction items for one assistant turn. Skips duplicate
ids, ``compaction`` checkpoints unless THIS request carries ``context_management`` (else a persisted
Expand Down Expand Up @@ -392,7 +404,17 @@ def _replay_reasoning_items(
)
_CROSS_ISSUER_WARN_EMITTED = True
continue
replayed.append({k: v for k, v in ri.items() if k not in ("id", "_issuer_kind", "_issuer_model")})
if is_azure_foundry:
replay_item = {
"type": "reasoning",
"encrypted_content": ri["encrypted_content"],
"summary": ri.get("summary") if isinstance(ri.get("summary"), list) else [],
}
if isinstance(item_id, str) and item_id:
replay_item["id"] = item_id
else:
replay_item = {k: v for k, v in ri.items() if k not in ("id", "_issuer_kind", "_issuer_model")}
replayed.append(replay_item)
if item_id:
seen_item_ids.add(item_id)
return replayed
Expand Down Expand Up @@ -493,6 +515,7 @@ def _chat_messages_to_responses_input(
messages: List[Dict[str, Any]], *, is_xai_responses: bool = False, is_github_responses: bool = False,
replay_encrypted_reasoning: bool = True, current_issuer_kind: Optional[str] = None,
current_issuer_model: Optional[str] = None, native_compaction_eligible: bool = False,
is_azure_foundry: bool = False,
) -> List[Dict[str, Any]]:
"""Convert internal chat-style messages to Responses input items.

Expand Down Expand Up @@ -565,22 +588,26 @@ def emit(new_items: List[Dict[str, Any]], msg: Dict[str, Any]) -> None:
reasoning_items = [] if not replay_encrypted_reasoning else _replay_reasoning_items(
msg, seen_item_ids=seen_item_ids, current_issuer_kind=current_issuer_kind,
current_issuer_model=current_issuer_model, native_compaction_eligible=native_compaction_eligible,
is_azure_foundry=is_azure_foundry,
)
emit(reasoning_items, msg)
message_items = _replay_message_items(
msg, is_github_responses=is_github_responses, current_issuer_kind=current_issuer_kind,
)
if is_azure_foundry:
for message_item in message_items:
_apply_azure_output_text_annotations(message_item.get("content", []))
emit(message_items, msg)
fallback = None
if not message_items:
fallback = content_parts or (content_text if content_text.strip() else "" if reasoning_items else None)
tool_items = _replay_tool_call_items(msg, start_index=len(items) + (fallback is not None), wire_ids=wire_ids)
# A function_call already follows its reasoning. Inventing an empty assistant
# message between them changes the replayed turn (Muse can emit corrupt finals).
# Keep a follower only for reasoning with no other following item, and make it
# non-empty: strict Responses-compatible providers reject "" with 400.
# A function_call already follows its reasoning; preserve the current-main
# follower ordering and only add a non-empty follower when needed.
if fallback is not None and not (fallback == "" and tool_items):
follower = " " if fallback == "" else fallback
if is_azure_foundry and isinstance(follower, list):
_apply_azure_output_text_annotations(follower)
emit([{"role": "assistant", "content": follower}], msg)
emit(tool_items, msg)
# The server renders nothing placed before a compaction item, so pre-checkpoint history is
Expand Down Expand Up @@ -694,7 +721,8 @@ def estimate_native_responses_preflight_tokens(
# --- Input preflight / validation --------------------------------------------

_PreflightCtx = NamedTuple("_PreflightCtx", [
("sanitize_text", Callable[[str], str]), ("sanitize_harmony_tokens", bool), ("is_github_responses", bool), ("seen_ids", set),
("sanitize_text", Callable[[str], str]), ("sanitize_harmony_tokens", bool), ("is_github_responses", bool),
("is_azure_foundry", bool), ("seen_ids", set),
])


Expand Down Expand Up @@ -744,10 +772,13 @@ def _preflight_encrypted(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) ->
return None
ctx.seen_ids.add(item_id)
summary = _as_list(item.get("summary"))
return {
reasoning_item = {
"type": "reasoning", "encrypted_content": encrypted,
"summary": _neutralize_harmony_structure(summary) if ctx.sanitize_harmony_tokens else summary,
}
if ctx.is_azure_foundry and _nonempty_str(item_id):
reasoning_item["id"] = item_id
return reasoning_item


def _preflight_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]:
Expand All @@ -766,6 +797,8 @@ def _preflight_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Di
f"Codex Responses input[{idx}] message content[{part_idx}] has unsupported type {part_type!r}."
)
normalized_content.append({"type": "output_text", "text": ctx.sanitize_text(_str_or_empty(part.get("text", "")))})
if ctx.is_azure_foundry:
_apply_azure_output_text_annotations(normalized_content)
if not normalized_content:
raise ValueError(f"Codex Responses input[{idx}] message item must contain at least one text part.")
return _assistant_message_item(item, normalized_content, is_github_responses=ctx.is_github_responses)
Expand Down Expand Up @@ -801,6 +834,8 @@ def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx)
raise ValueError(
f"Codex Responses input[{idx}].content[{part_idx}] has unsupported type {part.get('type')!r}."
)
if ctx.is_azure_foundry and role == "assistant":
_apply_azure_output_text_annotations(validated)
return {"role": role, "content": validated}


Expand All @@ -811,12 +846,13 @@ def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx)


def _preflight_codex_input_items(
raw_items: Any, *, is_github_responses: bool = False, sanitize_harmony_tokens: bool = False,
raw_items: Any, *, is_github_responses: bool = False, is_azure_foundry: bool = False,
sanitize_harmony_tokens: bool = False,
) -> List[Dict[str, Any]]:
if not isinstance(raw_items, list):
raise ValueError("Codex Responses input must be a list of input items.")
sanitize_text = _neutralize_harmony_tokens if sanitize_harmony_tokens else (lambda text: text)
ctx = _PreflightCtx(sanitize_text, sanitize_harmony_tokens, is_github_responses, set())
ctx = _PreflightCtx(sanitize_text, sanitize_harmony_tokens, is_github_responses, is_azure_foundry, set())
normalized: List[Dict[str, Any]] = []
for idx, item in enumerate(raw_items):
if not isinstance(item, dict):
Expand Down Expand Up @@ -880,7 +916,7 @@ def _optional_dict(api_kwargs: Dict[str, Any], key: str) -> Optional[Dict[str, A

def _preflight_codex_api_kwargs(
api_kwargs: Any, *, allow_stream: bool = False, is_github_responses: bool = False,
sanitize_harmony_tokens: bool = False,
is_azure_foundry: bool = False, sanitize_harmony_tokens: bool = False,
) -> Dict[str, Any]:
if not isinstance(api_kwargs, dict):
raise ValueError("Codex Responses request must be a dict.")
Expand All @@ -893,7 +929,8 @@ def _preflight_codex_api_kwargs(
if sanitize_harmony_tokens:
instructions = _neutralize_harmony_tokens(instructions)
input_items = _preflight_codex_input_items(
api_kwargs.get("input"), is_github_responses=is_github_responses, sanitize_harmony_tokens=sanitize_harmony_tokens,
api_kwargs.get("input"), is_github_responses=is_github_responses, is_azure_foundry=is_azure_foundry,
sanitize_harmony_tokens=sanitize_harmony_tokens,
)
normalized: Dict[str, Any] = {
"model": model.strip(), "instructions": instructions, "input": input_items, "store": False,
Expand Down
8 changes: 8 additions & 0 deletions agent/transports/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ def convert_messages(self, messages: list[dict[str, Any]], **kwargs) -> Any:
return _chat_messages_to_responses_input(
messages, is_xai_responses=kwargs.get("is_xai_responses") is True,
is_github_responses=kwargs.get("is_github_responses") is True,
is_azure_foundry=kwargs.get("is_azure_foundry") is True or _is_azure_responses(kwargs),
replay_encrypted_reasoning=bool(kwargs.get("replay_encrypted_reasoning", True)),
current_issuer_kind=self._resolve_issuer_kind(kwargs),
current_issuer_model=self._last_issuer_model,
Expand Down Expand Up @@ -566,6 +567,7 @@ def build_kwargs(
is_github_responses = params.get("is_github_responses") is True
is_codex_backend = params.get("is_codex_backend") is True
is_xai_responses = params.get("is_xai_responses") is True
is_azure_foundry = _is_azure_responses(params)
# Foundry 400s on encrypted-reasoning replay only in the post-tool follow-up turn.
replay_encrypted_reasoning = bool(params.get("replay_encrypted_reasoning", True)) and not (
_is_azure_foundry_responses(params) and _is_post_tool_replay(payload_messages)
Expand All @@ -592,6 +594,7 @@ def build_kwargs(
"instructions": instructions,
"input": self.convert_messages(
payload_messages, is_xai_responses=is_xai_responses, is_github_responses=is_github_responses,
is_azure_foundry=is_azure_foundry,
replay_encrypted_reasoning=replay_encrypted_reasoning, base_url=params.get("base_url"),
is_codex_backend=is_codex_backend, context_management=context_management, model=wire_model,
),
Expand Down Expand Up @@ -739,6 +742,7 @@ def validate_response(self, response: Any) -> bool:

def preflight_kwargs(
self, api_kwargs: Any, *, allow_stream: bool = False, is_github_responses: bool = False,
is_azure_foundry: bool = False, provider: str | None = None, base_url: str | None = None,
sanitize_harmony_tokens: bool = False,
) -> dict:
"""Validate and sanitize Codex API kwargs before the call.
Expand All @@ -747,8 +751,12 @@ def preflight_kwargs(
"""
from agent.codex_responses_adapter import _preflight_codex_api_kwargs

is_azure_foundry = is_azure_foundry or _is_azure_responses(
{"provider": provider, "base_url": base_url}
)
normalized = _preflight_codex_api_kwargs(
api_kwargs, allow_stream=allow_stream, is_github_responses=is_github_responses,
is_azure_foundry=is_azure_foundry,
sanitize_harmony_tokens=sanitize_harmony_tokens,
)
_bound_prompt_cache_key_field(normalized)
Expand Down
1 change: 1 addition & 0 deletions agent/turn_api_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def _perform_api_call(next_api_kwargs):
if agent.api_mode == "codex_responses":
next_api_kwargs = agent._get_transport().preflight_kwargs(
next_api_kwargs, allow_stream=False, is_github_responses=agent._is_copilot_url(),
provider=getattr(agent, "provider", None), base_url=getattr(agent, "base_url", None),
sanitize_harmony_tokens=agent._is_codex_backend(),
)
if _use_streaming:
Expand Down
1 change: 1 addition & 0 deletions agent/turn_api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def build_api_request(
if agent.api_mode == "codex_responses":
api_kwargs = agent._get_transport().preflight_kwargs(
api_kwargs, allow_stream=False, is_github_responses=agent._is_copilot_url(),
provider=getattr(agent, "provider", None), base_url=getattr(agent, "base_url", None),
sanitize_harmony_tokens=agent._is_codex_backend(),
)
# OpenRouter caching replays identical responses, even empty ones; an empty-response
Expand Down
130 changes: 130 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5160,3 +5160,133 @@ def test_only_titling_is_in_the_fast_tier(self):
_FAST_MODEL_TASKS
)
assert not overlap


class TestCodexAdapterAzureFoundryReasoningReplay:
"""Auxiliary Codex adapter must apply the Azure Foundry wire shape (#63257).

Auxiliary calls (compression, flush_memories, MoA) bypass
``ResponsesApiTransport.build_kwargs()``, so Foundry detection has to happen here
too — by client host OR by the live main runtime provider (a registered
``azure-foundry`` provider behind a proxy URL).
"""

_HISTORY = [
{"role": "system", "content": "You are helpful."},
{
"role": "assistant",
"content": [{"type": "text", "text": "thinking"}],
"codex_reasoning_items": [
{
"type": "reasoning", "id": "rs_123", "encrypted_content": "enc_blob",
"summary": [{"type": "summary_text", "text": "brief"}],
"status": "completed", "response_id": "resp_123",
}
],
},
{"role": "user", "content": "continue"},
]

@staticmethod
def _build_adapter(base_url):
from agent.auxiliary_client import _CodexCompletionsAdapter

message_item = SimpleNamespace(
type="message", role="assistant", status="completed",
content=[SimpleNamespace(type="output_text", text="ok")],
)
events = [
SimpleNamespace(type="response.created"),
SimpleNamespace(type="response.output_item.done", item=message_item),
SimpleNamespace(
type="response.completed",
response=SimpleNamespace(status="completed", id="resp_test", usage=None),
),
]

class _FakeCreateStream:
def __iter__(self):
return iter(events)

def close(self):
pass

captured = {}

def _create(**kwargs):
captured.update(kwargs)
return _FakeCreateStream()

real_client = MagicMock()
real_client.base_url = base_url
real_client.responses.create = _create
return _CodexCompletionsAdapter(real_client, "gpt-5.5"), captured

@staticmethod
def _wire_input(captured):
# ``_bypass_sdk_request_transform`` may route bulk ``input`` via ``extra_body``;
# the wire body is identical either way.
if "input" in captured:
return captured["input"]
return captured["extra_body"]["input"]

@classmethod
def _reasoning(cls, captured):
return next(item for item in cls._wire_input(captured) if item.get("type") == "reasoning")

@classmethod
def _assistant_text_part(cls, captured):
msg = next(
i for i in cls._wire_input(captured)
if i.get("role") == "assistant" and isinstance(i.get("content"), list)
)
return msg["content"][0]

@pytest.mark.parametrize(
"base_url",
["https://paperclip.services.ai.azure.com/models", "https://paperclip.openai.azure.com/openai/v1"],
)
def test_keeps_reasoning_id_for_azure_foundry_host(self, base_url):
from agent.auxiliary_client import clear_runtime_main

clear_runtime_main()
adapter, captured = self._build_adapter(base_url=base_url)
adapter.create(messages=list(self._HISTORY))

assert self._reasoning(captured) == {
"type": "reasoning", "id": "rs_123", "encrypted_content": "enc_blob",
"summary": [{"type": "summary_text", "text": "brief"}],
}
assert self._assistant_text_part(captured)["annotations"] == []

def test_keeps_reasoning_id_for_azure_foundry_runtime_provider_behind_proxy(self):
from agent.auxiliary_client import reset_runtime_main, set_runtime_main

token = set_runtime_main("azure-foundry", "gpt-5.5", base_url="https://gateway.corp.example/v1")
try:
adapter, captured = self._build_adapter(base_url="https://gateway.corp.example/v1")
adapter.create(messages=list(self._HISTORY))
finally:
reset_runtime_main(token)

assert self._reasoning(captured)["id"] == "rs_123"
assert self._assistant_text_part(captured)["annotations"] == []

@pytest.mark.parametrize(
"base_url",
[
"https://api.openai.com/v1",
"https://api.githubcopilot.com",
"https://evil.com/services.ai.azure.com/v1",
"https://openai.azure.com.evil.net/v1",
],
)
def test_non_foundry_host_keeps_default_shape(self, base_url):
from agent.auxiliary_client import clear_runtime_main

clear_runtime_main()
adapter, captured = self._build_adapter(base_url=base_url)
adapter.create(messages=list(self._HISTORY))

assert "id" not in self._reasoning(captured)
assert "annotations" not in self._assistant_text_part(captured)
Loading