diff --git a/.agents/evidence/pr/issue-415-codexapp-standard-responses/dashboard-standard-responses-capture.png b/.agents/evidence/pr/issue-415-codexapp-standard-responses/dashboard-standard-responses-capture.png new file mode 100644 index 00000000..8800d332 Binary files /dev/null and b/.agents/evidence/pr/issue-415-codexapp-standard-responses/dashboard-standard-responses-capture.png differ diff --git a/claude_tap/cli_clients.py b/claude_tap/cli_clients.py index 0ec3c352..4271231d 100644 --- a/claude_tap/cli_clients.py +++ b/claude_tap/cli_clients.py @@ -441,7 +441,10 @@ def reverse_strip_path_prefix(self, target: str) -> str: default_proxy_mode="forward", auto_trust_ca_macos=True, forward_trace_methods=("POST", "WEBSOCKET"), - forward_trace_path_prefixes=("/backend-api/codex/responses",), + forward_trace_path_prefixes=( + "/backend-api/codex/responses", + "/v1/responses", + ), ), "kimi": ClientConfig( cmd="kimi", diff --git a/tests/test_client_config_framework.py b/tests/test_client_config_framework.py index 74b26234..7dd203c7 100644 --- a/tests/test_client_config_framework.py +++ b/tests/test_client_config_framework.py @@ -134,7 +134,10 @@ def test_codexapp_declares_raw_backend_capture_mode() -> None: assert cfg.default_target == "https://chatgpt.com/backend-api/codex" assert cfg.default_proxy_mode == "forward" assert cfg.forward_trace_methods == ("POST", "WEBSOCKET") - assert cfg.forward_trace_path_prefixes == ("/backend-api/codex/responses",) + assert cfg.forward_trace_path_prefixes == ( + "/backend-api/codex/responses", + "/v1/responses", + ) assert cfg.transcript_only is False diff --git a/tests/test_forward_proxy.py b/tests/test_forward_proxy.py index 37d86fe5..1298acb6 100644 --- a/tests/test_forward_proxy.py +++ b/tests/test_forward_proxy.py @@ -10,6 +10,7 @@ import pytest +from claude_tap.cli_clients import CLIENT_CONFIGS from claude_tap.forward_proxy import ForwardProxyServer, _decode_request_body_for_trace from claude_tap.trace import TraceWriter from claude_tap.trace_store import get_trace_store, reset_trace_store @@ -36,6 +37,80 @@ async def drain(self) -> None: return None +@pytest.mark.parametrize( + ("method", "path", "expected"), + [ + ("POST", "/backend-api/codex/responses", True), + ("POST", "/v1/responses", True), + ("WEBSOCKET", "/v1/responses", True), + ("GET", "/v1/responses", False), + ("POST", "/v1/chat/completions", False), + ("POST", "/v1/responses-other", False), + ], +) +def test_codexapp_capture_filter(method: str, path: str, expected: bool) -> None: + cfg = CLIENT_CONFIGS["codexapp"] + server = ForwardProxyServer( + host="127.0.0.1", + port=0, + ca=object(), + writer=object(), + session=object(), + trace_methods=cfg.forward_trace_methods, + trace_path_prefixes=cfg.forward_trace_path_prefixes, + ) + + assert server._should_trace_request(method, path) is expected + + +@pytest.mark.asyncio +async def test_forward_proxy_captures_codexapp_custom_responses_request( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "traces.sqlite3")) + reset_trace_store() + store = get_trace_store() + session_id = store.create_session(client="codexapp", proxy_mode="forward") + trace_writer = TraceWriter(session_id, store=store) + client_writer = _MemoryWriter() + cfg = CLIENT_CONFIGS["codexapp"] + server = ForwardProxyServer( + host="127.0.0.1", + port=0, + ca=object(), + writer=trace_writer, + session=_UnexpectedSession(), + trace_methods=cfg.forward_trace_methods, + trace_path_prefixes=cfg.forward_trace_path_prefixes, + capture_only=True, + ) + request_body = { + "model": "MiniMax-M3", + "input": [{"role": "user", "content": [{"type": "input_text", "text": "hello"}]}], + "stream": False, + } + + try: + await server._forward_and_record( + "POST", + "/v1/responses", + {"Content-Type": "application/json"}, + json.dumps(request_body).encode(), + "https://api.minimaxi.com/v1/responses", + client_writer, + ) + trace_writer.close() + + records = store.load_records(session_id) + assert len(records) == 1 + assert records[0]["request"]["path"] == "/v1/responses" + assert records[0]["request"]["body"] == request_body + assert records[0]["response"]["status"] == 200 + finally: + reset_trace_store() + + @pytest.mark.parametrize( ("encoding", "compress"), [