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
18 changes: 17 additions & 1 deletion agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1240,14 +1240,30 @@ def dump_api_request_debug(
api_key = getattr(agent.client, "api_key", None)
except Exception as e:
_ra().logger.debug("Could not extract API key for debug dump: %s", e)
# Anthropic-mode providers store the client in
# ``agent._anthropic_client`` and set ``agent.client = None``.
# Fall back to ``agent.api_key`` (set by init_agent for all modes)
# so the dump shows a masked key instead of ``Bearer None``.
if not api_key:
api_key = getattr(agent, "api_key", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anthropic_messages, prefer agent._anthropic_api_key before agent.api_key: native credential refresh rebuilds the live client and updates only _anthropic_api_key (run_agent.py:4406-4416), so this fallback can mask a stale rather than active credential.


# Build the dump URL to match the actual outbound endpoint.
# Anthropic SDK posts to ``/v1/messages`` (base_url already has /v1
# stripped by the normalizer); Codex uses ``/responses``.
if agent.api_mode == "anthropic_messages":
_dump_url = f"{agent.base_url.rstrip('/')}/v1/messages"
elif agent.api_mode == "codex_responses":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalize a trailing /v1 before appending /v1/messages. The live Anthropic adapter strips that suffix (agent/anthropic_adapter.py:757-760), so a valid configured https://api.anthropic.com/v1 would otherwise be dumped as /v1/v1/messages.

_dump_url = f"{agent.base_url.rstrip('/')}/responses"
else:
_dump_url = f"{agent.base_url.rstrip('/')}/chat/completions"

dump_payload: Dict[str, Any] = {
"timestamp": datetime.now().isoformat(),
"session_id": agent.session_id,
"reason": reason,
"request": {
"method": "POST",
"url": f"{agent.base_url.rstrip('/')}{'/responses' if agent.api_mode == 'codex_responses' else '/chat/completions'}",
"url": _dump_url,
"headers": {
"Authorization": f"Bearer {agent._mask_api_key_for_logs(api_key)}",
"Content-Type": "application/json",
Expand Down
71 changes: 71 additions & 0 deletions tests/run_agent/test_run_agent_codex_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,77 @@ class ProviderError(RuntimeError):
assert "***" in dumped_text or "..." in dumped_text


def test_dump_api_request_debug_anthropic_mode_shows_masked_key(monkeypatch, tmp_path):
"""Debug dumps for anthropic_messages mode should show a masked key, not 'None'.

For Anthropic-mode providers (MiniMax, Kimi, etc.) ``agent.client`` is
``None`` β€” the real client lives in ``agent._anthropic_client``. The dump
must fall back to ``agent.api_key`` so the Authorization header shows a
masked key instead of ``Bearer None``.
"""
import json

_patch_agent_bootstrap(monkeypatch)
agent = run_agent.AIAgent(
model="MiniMax-M3",
provider="minimax-cn",
api_mode="anthropic_messages",
base_url="https://api.minimaxi.com/anthropic",
api_key="sk-cp-abcdef0123456789",
quiet_mode=True,
max_iterations=1,
skip_context_files=True,
skip_memory=True,
)
agent.logs_dir = tmp_path
# Anthropic mode sets agent.client = None
assert agent.client is None

dump_file = agent._dump_api_request_debug(
{"model": "MiniMax-M3", "messages": [{"role": "user", "content": "hi"}]},
reason="preflight",
)

assert dump_file is not None
payload = json.loads(dump_file.read_text())
auth_header = payload["request"]["headers"]["Authorization"]
# Must NOT be "Bearer None"
assert auth_header != "Bearer None", f"Expected masked key, got: {auth_header}"
# Must show a masked representation of the key
assert auth_header.startswith("Bearer sk-cp-ab...")
assert "None" not in auth_header


def test_dump_api_request_debug_anthropic_mode_shows_messages_url(monkeypatch, tmp_path):
"""Debug dumps for anthropic_messages mode should show /v1/messages URL.

The Anthropic SDK posts to ``/v1/messages``, not ``/chat/completions``.
"""
import json

_patch_agent_bootstrap(monkeypatch)
agent = run_agent.AIAgent(
model="MiniMax-M3",
provider="minimax-cn",
api_mode="anthropic_messages",
base_url="https://api.minimaxi.com/anthropic",
api_key="sk-cp-abcdef0123456789",
quiet_mode=True,
max_iterations=1,
skip_context_files=True,
skip_memory=True,
)
agent.logs_dir = tmp_path

dump_file = agent._dump_api_request_debug(
{"model": "MiniMax-M3", "messages": [{"role": "user", "content": "hi"}]},
reason="preflight",
)

payload = json.loads(dump_file.read_text())
assert payload["request"]["url"] == "https://api.minimaxi.com/anthropic/v1/messages"


# --- Reasoning-only response tests (fix for empty content retry loop) ---


Expand Down
Loading