diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc deleted file mode 100644 index fb4104d..0000000 --- a/.cursor/rules/general.mdc +++ /dev/null @@ -1,13 +0,0 @@ ---- -alwaysApply: true ---- - -## Running Python scripts - -By default, the Python packages defined in this repository will be installed inside this `pytools` Conda environment. - -For example, you can run: - -```bash -source /root/miniconda3/etc/profile.d/conda.sh && conda activate pytools && your_commands_here -``` diff --git a/README.md b/README.md index bfd1dfc..d237730 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,31 @@ Note: you can toggle the custom API on and off with: Install and run the proxy: ```bash +# Or, use your favourite Python package manager conda create -n dcp python=3.10 -y conda activate dcp + +# Install pip install -e . -deepseek-cursor-proxy --verbose + +# Run in normal mode +deepseek-cursor-proxy ``` The proxy creates `~/.deepseek-cursor-proxy/config.yaml` on first run. This will also print the ngrok public URL. If it differs from the one in Cursor, update it in Cursor's Base URL field. +Normal mode prints startup info, the ngrok URL, and safe request summaries. It does not print prompts, code, API keys, or request bodies. + +For more request lifecycle metadata, use verbose mode: + +```bash +deepseek-cursor-proxy --verbose +``` + +Verbose mode adds client/path/upstream metadata and full payload logs. It may print prompts and code to the terminal, so keep it off for normal use. + ### Step 4: Chat with DeepSeek in Cursor Select `deepseek-v4-pro` in Cursor and use chat or agent mode as usual. diff --git a/config.example.yaml b/config.example.yaml index 30dcd27..f5c950c 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -11,7 +11,6 @@ host: 127.0.0.1 port: 9000 ngrok: true verbose: false -log_bodies: false request_timeout: 300 reasoning_content_path: reasoning_content.sqlite3 diff --git a/src/deepseek_cursor_proxy/config.py b/src/deepseek_cursor_proxy/config.py index 8ea44b3..dc00a06 100644 --- a/src/deepseek_cursor_proxy/config.py +++ b/src/deepseek_cursor_proxy/config.py @@ -28,7 +28,6 @@ host: 127.0.0.1 port: 9000 ngrok: true verbose: false -log_bodies: false request_timeout: 300 reasoning_content_path: reasoning_content.sqlite3 @@ -167,7 +166,6 @@ class ProxyConfig: reasoning_content_path: Path = field(default_factory=default_reasoning_content_path) cursor_display_reasoning: bool = True verbose: bool = False - log_bodies: bool = False ngrok: bool = False @classmethod @@ -290,15 +288,6 @@ class ProxyConfig: ), False, ), - log_bodies=as_bool( - setting_value( - settings, - live_env, - "log_bodies", - "PROXY_LOG_BODIES", - ), - False, - ), ngrok=as_bool( setting_value( settings, diff --git a/src/deepseek_cursor_proxy/server.py b/src/deepseek_cursor_proxy/server.py index d130e66..339622a 100644 --- a/src/deepseek_cursor_proxy/server.py +++ b/src/deepseek_cursor_proxy/server.py @@ -84,12 +84,17 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): self.headers.get("User-Agent", ""), ) if request_path not in {"/chat/completions", "/v1/chat/completions"}: + LOG.warning("rejected unsupported POST path=%s status=404", request_path) self._send_json( 404, {"error": {"message": "Only /v1/chat/completions is supported"}} ) return cursor_authorization = self._cursor_authorization() if cursor_authorization is None: + LOG.warning( + "rejected request path=%s status=401 reason=missing_bearer_token", + request_path, + ) self._send_json( 401, {"error": {"message": "Missing Authorization bearer token"}} ) @@ -98,14 +103,16 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): try: payload = self._read_json_body() except ValueError as exc: + LOG.warning( + "rejected request path=%s status=400 reason=%s", request_path, exc + ) self._send_json(400, {"error": {"message": str(exc)}}) return - if self.config.log_bodies: + if self.config.verbose: log_json("cursor request body", payload) - if self.config.verbose: - LOG.info("cursor request: %s", summarize_chat_payload(payload)) + LOG.info("cursor request: %s", summarize_chat_payload(payload)) prepared = prepare_upstream_request(payload, self.config, self.reasoning_store) if prepared.patched_reasoning_messages: @@ -132,7 +139,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): summarize_chat_payload(prepared.payload), ) - if self.config.log_bodies: + if self.config.verbose: log_json("upstream request body", prepared.payload) upstream_body = json.dumps( @@ -154,31 +161,31 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): LOG.info("forwarding to %s", upstream_url) response = urlopen(request, timeout=self.config.request_timeout) except HTTPError as exc: - if self.config.verbose: - LOG.info( - "upstream error status=%s elapsed_ms=%s", - exc.code, - elapsed_ms(started), - ) + LOG.warning( + "request failed upstream_status=%s stream=%s elapsed_ms=%s", + exc.code, + bool(prepared.payload.get("stream")), + elapsed_ms(started), + ) self._send_upstream_error(exc) return except URLError as exc: - if self.config.verbose: - LOG.warning( - "upstream request failed elapsed_ms=%s reason=%s", - elapsed_ms(started), - exc.reason, - ) + LOG.warning( + "upstream request failed elapsed_ms=%s reason=%s", + elapsed_ms(started), + exc.reason, + ) self._send_json( 502, {"error": {"message": f"Upstream request failed: {exc.reason}"}} ) return with response: + upstream_status = getattr(response, "status", 200) if self.config.verbose: LOG.info( "upstream response status=%s stream=%s elapsed_ms=%s", - getattr(response, "status", 200), + upstream_status, bool(prepared.payload.get("stream")), elapsed_ms(started), ) @@ -190,6 +197,17 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): self._proxy_regular_response( response, prepared.original_model, prepared.payload["messages"] ) + LOG.info( + ( + "request complete status=%s stream=%s elapsed_ms=%s " + "patched_reasoning=%s fallback_reasoning=%s" + ), + upstream_status, + bool(prepared.payload.get("stream")), + elapsed_ms(started), + prepared.patched_reasoning_messages, + prepared.fallback_reasoning_messages, + ) def _cursor_authorization(self) -> str | None: auth_header = self.headers.get("Authorization", "") @@ -259,7 +277,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): def _send_upstream_error(self, exc: HTTPError) -> None: body = read_response_body(exc) - if self.config.log_bodies: + if self.config.verbose: log_bytes("upstream error body", body) self.send_response(exc.code) self._send_cors_headers() @@ -284,7 +302,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): except (json.JSONDecodeError, UnicodeDecodeError) as exc: LOG.warning("failed to rewrite upstream JSON response: %s", exc) - if self.config.log_bodies: + if self.config.verbose: log_bytes("cursor response body", body) self.send_response(getattr(response, "status", 200)) @@ -331,7 +349,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): break if not finalized: - if self.config.log_bodies: + if self.config.verbose: log_json("model streaming assistant messages", accumulator.messages()) stored = accumulator.store_reasoning(self.reasoning_store, scope) if stored: @@ -351,7 +369,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler): data = stripped[len(b"data:") :].strip() if data == b"[DONE]": - if self.config.log_bodies: + if self.config.verbose: log_json("model streaming assistant messages", accumulator.messages()) stored = accumulator.store_reasoning(self.reasoning_store, scope) if stored: @@ -425,12 +443,7 @@ def build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "--verbose", action="store_true", - help="Log request lifecycle metadata without bodies", - ) - parser.add_argument( - "--log-bodies", - action="store_true", - help="Log normalized upstream request bodies", + help="Log detailed request lifecycle metadata and full payloads", ) parser.add_argument( "--no-cursor-display-reasoning", @@ -521,8 +534,6 @@ def main(argv: list[str] | None = None) -> int: updates["ngrok"] = True if args.verbose: updates["verbose"] = True - if args.log_bodies: - updates["log_bodies"] = True if args.no_cursor_display_reasoning: updates["cursor_display_reasoning"] = False if updates: @@ -547,11 +558,12 @@ def main(argv: list[str] | None = None) -> int: config.reasoning_content_path, ) if config.verbose: - LOG.info("verbose logging enabled") - if config.log_bodies: + LOG.info("logging mode=verbose metadata=detailed bodies=true") LOG.warning( - "request body logging enabled; prompts and code may be written to stdout" + "verbose logging enabled; prompts and code may be written to stdout" ) + else: + LOG.info("logging mode=normal metadata=safe_summaries bodies=false") tunnel: NgrokTunnel | None = None if config.ngrok: diff --git a/tests/test_config.py b/tests/test_config.py index 688667b..4394c9a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -135,18 +135,16 @@ class ConfigTests(unittest.TestCase): home / ".deepseek-cursor-proxy" / "custom.sqlite3", ) - def test_verbose_and_body_logging_can_be_enabled_from_env(self) -> None: + def test_verbose_logging_can_be_enabled_from_env(self) -> None: config = ProxyConfig.from_file( env={ "PROXY_VERBOSE": "true", - "PROXY_LOG_BODIES": "1", "PROXY_NGROK": "yes", }, config_path=Path("/does/not/exist"), ) self.assertTrue(config.verbose) - self.assertTrue(config.log_bodies) self.assertTrue(config.ngrok) def test_cursor_reasoning_display_can_be_disabled_from_config(self) -> None: diff --git a/tests/test_proxy_end_to_end.py b/tests/test_proxy_end_to_end.py index 8ab5048..b1aa714 100644 --- a/tests/test_proxy_end_to_end.py +++ b/tests/test_proxy_end_to_end.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import replace from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import threading @@ -462,6 +463,40 @@ class ProxyEndToEndTests(unittest.TestCase): self.assertEqual(status, 200) self.assertEqual(FakeDeepSeekHandler.auth_headers[0], "Bearer sk-from-cursor") + def test_normal_mode_logs_safe_request_progress_without_bodies(self) -> None: + with self.assertLogs("deepseek_cursor_proxy", level="INFO") as captured: + status, _ = post_json( + f"{self.proxy.url}/v1/chat/completions", + first_cursor_request(), + api_key="sk-from-cursor", + ) + + output = "\n".join(captured.output) + self.assertEqual(status, 200) + self.assertIn("cursor request: model='deepseek-v4-pro'", output) + self.assertIn("request complete status=200", output) + self.assertNotIn("What is tomorrow's date?", output) + self.assertNotIn("sk-from-cursor", output) + + def test_verbose_mode_logs_metadata_and_bodies_without_api_key(self) -> None: + self.proxy.server.config = replace(self.proxy.server.config, verbose=True) + + with self.assertLogs("deepseek_cursor_proxy", level="INFO") as captured: + status, _ = post_json( + f"{self.proxy.url}/v1/chat/completions", + first_cursor_request(), + api_key="sk-from-cursor", + ) + + output = "\n".join(captured.output) + self.assertEqual(status, 200) + self.assertIn("incoming POST /v1/chat/completions", output) + self.assertIn("upstream request metadata", output) + self.assertIn("cursor request body", output) + self.assertIn("upstream request body", output) + self.assertIn("What is tomorrow's date?", output) + self.assertNotIn("sk-from-cursor", output) + def test_proxy_rejects_missing_cursor_bearer_token(self) -> None: request = Request( f"{self.proxy.url}/v1/chat/completions",