Skip to content
Open
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
46 changes: 3 additions & 43 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pathlib import Path
from urllib.parse import urlparse

from agent.message_content import to_plain_data as _message_value_to_plain_data
from hermes_constants import get_hermes_home
from typing import Any, Dict, List, Optional, Tuple
from utils import base_url_host_matches, normalize_proxy_env_vars
Expand Down Expand Up @@ -1753,49 +1754,8 @@ def _convert_content_part_to_anthropic(part: Any) -> Optional[Dict[str, Any]]:


def _to_plain_data(value: Any, *, _depth: int = 0, _path: Optional[set] = None) -> Any:
"""Recursively convert SDK objects to plain Python data structures.

Guards against circular references (``_path`` tracks ``id()`` of objects
on the *current* recursion path) and runaway depth (capped at 20 levels).
Uses path-based tracking so shared (but non-cyclic) objects referenced by
multiple siblings are converted correctly rather than being stringified.
"""
_MAX_DEPTH = 20
if _depth > _MAX_DEPTH:
return str(value)

if _path is None:
_path = set()

obj_id = id(value)
if obj_id in _path:
return str(value)

if hasattr(value, "model_dump"):
_path.add(obj_id)
result = _to_plain_data(value.model_dump(), _depth=_depth + 1, _path=_path)
_path.discard(obj_id)
return result
if isinstance(value, dict):
_path.add(obj_id)
result = {k: _to_plain_data(v, _depth=_depth + 1, _path=_path) for k, v in value.items()}
_path.discard(obj_id)
return result
if isinstance(value, (list, tuple)):
_path.add(obj_id)
result = [_to_plain_data(v, _depth=_depth + 1, _path=_path) for v in value]
_path.discard(obj_id)
return result
if hasattr(value, "__dict__"):
_path.add(obj_id)
result = {
k: _to_plain_data(v, _depth=_depth + 1, _path=_path)
for k, v in vars(value).items()
if not k.startswith("_")
}
_path.discard(obj_id)
return result
return value
"""Compatibility wrapper around the provider-neutral SDK converter."""
return _message_value_to_plain_data(value, _depth=_depth, _path=_path)


def _extract_preserved_thinking_blocks(message: Dict[str, Any]) -> List[Dict[str, Any]]:
Expand Down
25 changes: 22 additions & 3 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,19 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
"""Build the keyword arguments dict for the active API mode."""
tools_for_api = agent.tools

reasoning_config = getattr(agent, "reasoning_config", None)
ultra_requested = (
isinstance(reasoning_config, dict)
and reasoning_config.get("enabled") is not False
and str(reasoning_config.get("effort") or "").strip().lower() == "ultra"
)
if ultra_requested and agent.api_mode not in {"codex_responses", "codex_app_server"}:
raise ValueError(
"Ultra is only supported by the OpenAI Responses Multi-agent beta "
"or the Codex app-server runtime. Choose another reasoning effort "
"for this provider."
)

if agent.api_mode == "anthropic_messages":
_transport = agent._get_transport()
anthropic_messages = agent._prepare_anthropic_messages_for_api(api_messages)
Expand Down Expand Up @@ -736,6 +749,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
and "/backend-api/codex" in agent._base_url_lower
)
)
is_openai_api = agent._base_url_hostname == "api.openai.com"
is_xai_responses = agent.provider in {"xai", "xai-oauth"} or agent._base_url_hostname == "api.x.ai"
_msgs_for_codex = agent._prepare_messages_for_non_vision_model(api_messages)

Expand Down Expand Up @@ -780,8 +794,12 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
max_tokens=agent.max_tokens,
timeout=agent._resolved_api_call_timeout(),
request_overrides=agent.request_overrides,
provider=agent.provider,
base_url=agent.base_url,
base_url_hostname=agent._base_url_hostname,
is_github_responses=is_github_responses,
is_codex_backend=is_codex_backend,
is_openai_api=is_openai_api,
is_xai_responses=is_xai_responses,
github_reasoning_extra=agent._github_models_reasoning_extra_body() if is_github_responses else None,
replay_encrypted_reasoning=bool(
Expand Down Expand Up @@ -1100,9 +1118,10 @@ def build_assistant_message(agent, assistant_message, finish_reason: str) -> dic
if codex_items:
msg["codex_reasoning_items"] = codex_items

# Codex Responses API: preserve exact assistant message items (with
# id/phase) so follow-up turns can replay structured items instead of
# flattening to plain text. This is required for prefix cache hits.
# Codex Responses API: preserve replayable output items. Normal turns carry
# exact assistant message items (id/phase) for prefix-cache continuity;
# Multi-agent turns carry the complete ordered output list so hosted actions,
# agent messages, and agent-attributed function calls survive continuation.
codex_message_items = getattr(assistant_message, "codex_message_items", None)
if codex_message_items:
msg["codex_message_items"] = codex_message_items
Expand Down
Loading