diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 0b04eb38c83cc..1f669892d8f2c 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -1240,6 +1240,22 @@ 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) + + # 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": + _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(), @@ -1247,7 +1263,7 @@ def dump_api_request_debug( "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", diff --git a/tests/run_agent/test_run_agent_codex_responses.py b/tests/run_agent/test_run_agent_codex_responses.py index 14e01d9fecd50..0b53fe80e35da 100644 --- a/tests/run_agent/test_run_agent_codex_responses.py +++ b/tests/run_agent/test_run_agent_codex_responses.py @@ -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) ---