Skip to content
Merged
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
33 changes: 32 additions & 1 deletion src/deepseek_cursor_proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def do_POST(self) -> None:
)
if request_path not in {"/chat/completions", "/v1/chat/completions"}:
LOG.warning("rejected unsupported POST path=%s status=404", request_path)
self._record_request_body_for_trace(trace)
self._send_json(
404,
{"error": {"message": "Only /v1/chat/completions is supported"}},
Expand All @@ -119,6 +120,7 @@ def do_POST(self) -> None:
"rejected request path=%s status=401 reason=missing_bearer_token",
request_path,
)
self._record_request_body_for_trace(trace)
self._send_json(
401,
{"error": {"message": "Missing Authorization bearer token"}},
Expand Down Expand Up @@ -161,7 +163,10 @@ def do_POST(self) -> None:
if trace is not None:
trace.record_transform(prepared)
log_context_summary(prepared)
if prepared.missing_reasoning_messages:
if (
prepared.missing_reasoning_messages
and self.config.missing_reasoning_strategy == "reject"
):
LOG.warning(
(
"strict missing-reasoning mode rejected request path=%s "
Expand Down Expand Up @@ -470,6 +475,32 @@ def _read_json_body(self) -> dict[str, Any]:
raise ValueError("Request body must be a JSON object")
return payload

def _record_request_body_for_trace(self, trace: TraceRequest | None) -> None:
if trace is None:
return
try:
length = int(self.headers.get("Content-Length") or 0)
except ValueError:
trace.record_cursor_body_omitted(reason="invalid_content_length")
return
if length < 0:
trace.record_cursor_body_omitted(
reason="invalid_content_length", body_bytes=length
)
return
if length > self.config.max_request_body_bytes:
trace.record_cursor_body_omitted(reason="body_too_large", body_bytes=length)
self.close_connection = True
return
try:
raw_body = self.rfile.read(length)
except OSError as exc:
trace.record_cursor_body_omitted(
reason=f"read_failed:{exc}", body_bytes=length
)
return
trace.record_cursor_body_bytes(raw_body)

def _upstream_headers(self, stream: bool, authorization: str) -> dict[str, str]:
headers = {
"Authorization": authorization,
Expand Down
22 changes: 21 additions & 1 deletion src/deepseek_cursor_proxy/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def _write_manifest(self) -> None:
"pid": os.getpid(),
"base_dir": str(self.base_dir),
"session_dir": str(self.session_dir),
"format": "one JSON file per proxied POST request",
"format": "one JSON file per traced POST request",
},
)

Expand All @@ -224,6 +224,26 @@ def record_cursor_body(self, payload: dict[str, Any]) -> None:
self.data["request"]["body"] = payload
self.data["request"]["summary"] = payload_summary(payload)

def record_cursor_body_bytes(self, body: bytes) -> None:
self.data["request"]["body_bytes"] = len(body)
text = body.decode("utf-8", errors="replace")
try:
payload = json.loads(text)
except json.JSONDecodeError:
self.data["request"]["body"] = {"text": text}
return
self.data["request"]["body"] = payload
if isinstance(payload, dict):
self.data["request"]["summary"] = payload_summary(payload)

def record_cursor_body_omitted(
self, *, reason: str, body_bytes: int | None = None
) -> None:
omitted: dict[str, Any] = {"reason": reason}
if body_bytes is not None:
omitted["body_bytes"] = body_bytes
self.data["request"]["body_omitted"] = omitted

def record_transform(self, prepared: Any) -> None:
self.data["transform"] = {
"original_model": prepared.original_model,
Expand Down
37 changes: 37 additions & 0 deletions tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,43 @@ def test_recovery_notice_is_stripped_before_upstream_replay(self) -> None:
continue
self.assertNotIn("deepseek-cursor-proxy", message.get("content", ""))

def test_recover_mode_does_not_short_circuit_with_409(self) -> None:
"""In `recover` mode, a payload with no user message leaves the
recovery loop unable to drop anything (`dropped_messages == 0`),
so `missing_indexes` stays populated. The proxy must NOT 409 in
that case — it must forward to upstream and relay whatever
DeepSeek decides. 409 is reserved for `reject` mode."""
status, _ = _post(
f"{self.proxy.url}/v1/chat/completions",
{
"model": "deepseek-v4-pro",
"messages": [
{"role": "system", "content": "Be brief."},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": CALL_ID_1,
"type": "function",
"function": {"name": "get_date", "arguments": "{}"},
}
],
},
{
"role": "tool",
"tool_call_id": CALL_ID_1,
"content": "2026-04-24",
},
],
},
)
# Strict upstream rejects the missing-reasoning history with 400.
# The point of this test is the proxy did NOT pre-empt with 409.
self.assertNotEqual(status, 409)
self.assertEqual(status, 400)
self.assertEqual(len(StrictFakeDeepSeek.requests), 1)


# ---------------------------------------------------------------------------
# Streaming behaviour
Expand Down
31 changes: 31 additions & 0 deletions tests/test_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from tempfile import TemporaryDirectory
import time
import unittest
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from deepseek_cursor_proxy.config import ProxyConfig
Expand Down Expand Up @@ -207,6 +208,36 @@ def _post(self, payload: dict) -> dict:
with urlopen(request, timeout=5) as response:
return json.loads(response.read())

def test_traces_unsupported_post_path_with_body(self) -> None:
request = Request(
f"{self.proxy.url}/v1/summarize",
data=json.dumps(
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "summarize"}],
}
).encode("utf-8"),
method="POST",
headers={
"Authorization": "Bearer sk-from-cursor",
"Content-Type": "application/json",
},
)
with self.assertRaises(HTTPError) as captured:
urlopen(request, timeout=5)
self.assertEqual(captured.exception.code, 404)
captured.exception.read()

trace = _read_single_trace(self.writer.session_dir)
self.assertEqual(trace["request"]["method"], "POST")
self.assertEqual(trace["request"]["path"], "/v1/summarize")
self.assertEqual(trace["request"]["body"]["model"], "gpt-4o-mini")
self.assertEqual(trace["request"]["summary"]["model"], "gpt-4o-mini")
self.assertEqual(trace["completion"]["status"], "rejected")
self.assertEqual(trace["completion"]["http_status"], 404)
self.assertEqual(trace["transform"], {})
self.assertEqual(_CannedUpstream.requests, [])

def test_captures_non_streaming_replay_without_api_key(self) -> None:
self._post(
{
Expand Down
Loading