refactor(logging): merge log_bodies into verbose mode and improve log levels (#4)

main
Yixing Lao 2026-04-24 18:53:24 +08:00 committed by GitHub
parent c238a40045
commit 08eeb87c48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 96 additions and 61 deletions

View File

@ -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
```

View File

@ -69,16 +69,31 @@ Note: you can toggle the custom API on and off with:
Install and run the proxy: Install and run the proxy:
```bash ```bash
# Or, use your favourite Python package manager
conda create -n dcp python=3.10 -y conda create -n dcp python=3.10 -y
conda activate dcp conda activate dcp
# Install
pip install -e . 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. 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. 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 ### Step 4: Chat with DeepSeek in Cursor
Select `deepseek-v4-pro` in Cursor and use chat or agent mode as usual. Select `deepseek-v4-pro` in Cursor and use chat or agent mode as usual.

View File

@ -11,7 +11,6 @@ host: 127.0.0.1
port: 9000 port: 9000
ngrok: true ngrok: true
verbose: false verbose: false
log_bodies: false
request_timeout: 300 request_timeout: 300
reasoning_content_path: reasoning_content.sqlite3 reasoning_content_path: reasoning_content.sqlite3

View File

@ -28,7 +28,6 @@ host: 127.0.0.1
port: 9000 port: 9000
ngrok: true ngrok: true
verbose: false verbose: false
log_bodies: false
request_timeout: 300 request_timeout: 300
reasoning_content_path: reasoning_content.sqlite3 reasoning_content_path: reasoning_content.sqlite3
@ -167,7 +166,6 @@ class ProxyConfig:
reasoning_content_path: Path = field(default_factory=default_reasoning_content_path) reasoning_content_path: Path = field(default_factory=default_reasoning_content_path)
cursor_display_reasoning: bool = True cursor_display_reasoning: bool = True
verbose: bool = False verbose: bool = False
log_bodies: bool = False
ngrok: bool = False ngrok: bool = False
@classmethod @classmethod
@ -290,15 +288,6 @@ class ProxyConfig:
), ),
False, False,
), ),
log_bodies=as_bool(
setting_value(
settings,
live_env,
"log_bodies",
"PROXY_LOG_BODIES",
),
False,
),
ngrok=as_bool( ngrok=as_bool(
setting_value( setting_value(
settings, settings,

View File

@ -84,12 +84,17 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
self.headers.get("User-Agent", ""), self.headers.get("User-Agent", ""),
) )
if request_path not in {"/chat/completions", "/v1/chat/completions"}: if request_path not in {"/chat/completions", "/v1/chat/completions"}:
LOG.warning("rejected unsupported POST path=%s status=404", request_path)
self._send_json( self._send_json(
404, {"error": {"message": "Only /v1/chat/completions is supported"}} 404, {"error": {"message": "Only /v1/chat/completions is supported"}}
) )
return return
cursor_authorization = self._cursor_authorization() cursor_authorization = self._cursor_authorization()
if cursor_authorization is None: if cursor_authorization is None:
LOG.warning(
"rejected request path=%s status=401 reason=missing_bearer_token",
request_path,
)
self._send_json( self._send_json(
401, {"error": {"message": "Missing Authorization bearer token"}} 401, {"error": {"message": "Missing Authorization bearer token"}}
) )
@ -98,13 +103,15 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
try: try:
payload = self._read_json_body() payload = self._read_json_body()
except ValueError as exc: 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)}}) self._send_json(400, {"error": {"message": str(exc)}})
return return
if self.config.log_bodies: if self.config.verbose:
log_json("cursor request body", payload) 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) prepared = prepare_upstream_request(payload, self.config, self.reasoning_store)
@ -132,7 +139,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
summarize_chat_payload(prepared.payload), summarize_chat_payload(prepared.payload),
) )
if self.config.log_bodies: if self.config.verbose:
log_json("upstream request body", prepared.payload) log_json("upstream request body", prepared.payload)
upstream_body = json.dumps( upstream_body = json.dumps(
@ -154,16 +161,15 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
LOG.info("forwarding to %s", upstream_url) LOG.info("forwarding to %s", upstream_url)
response = urlopen(request, timeout=self.config.request_timeout) response = urlopen(request, timeout=self.config.request_timeout)
except HTTPError as exc: except HTTPError as exc:
if self.config.verbose: LOG.warning(
LOG.info( "request failed upstream_status=%s stream=%s elapsed_ms=%s",
"upstream error status=%s elapsed_ms=%s",
exc.code, exc.code,
bool(prepared.payload.get("stream")),
elapsed_ms(started), elapsed_ms(started),
) )
self._send_upstream_error(exc) self._send_upstream_error(exc)
return return
except URLError as exc: except URLError as exc:
if self.config.verbose:
LOG.warning( LOG.warning(
"upstream request failed elapsed_ms=%s reason=%s", "upstream request failed elapsed_ms=%s reason=%s",
elapsed_ms(started), elapsed_ms(started),
@ -175,10 +181,11 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
return return
with response: with response:
upstream_status = getattr(response, "status", 200)
if self.config.verbose: if self.config.verbose:
LOG.info( LOG.info(
"upstream response status=%s stream=%s elapsed_ms=%s", "upstream response status=%s stream=%s elapsed_ms=%s",
getattr(response, "status", 200), upstream_status,
bool(prepared.payload.get("stream")), bool(prepared.payload.get("stream")),
elapsed_ms(started), elapsed_ms(started),
) )
@ -190,6 +197,17 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
self._proxy_regular_response( self._proxy_regular_response(
response, prepared.original_model, prepared.payload["messages"] 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: def _cursor_authorization(self) -> str | None:
auth_header = self.headers.get("Authorization", "") auth_header = self.headers.get("Authorization", "")
@ -259,7 +277,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
def _send_upstream_error(self, exc: HTTPError) -> None: def _send_upstream_error(self, exc: HTTPError) -> None:
body = read_response_body(exc) body = read_response_body(exc)
if self.config.log_bodies: if self.config.verbose:
log_bytes("upstream error body", body) log_bytes("upstream error body", body)
self.send_response(exc.code) self.send_response(exc.code)
self._send_cors_headers() self._send_cors_headers()
@ -284,7 +302,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
except (json.JSONDecodeError, UnicodeDecodeError) as exc: except (json.JSONDecodeError, UnicodeDecodeError) as exc:
LOG.warning("failed to rewrite upstream JSON response: %s", 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) log_bytes("cursor response body", body)
self.send_response(getattr(response, "status", 200)) self.send_response(getattr(response, "status", 200))
@ -331,7 +349,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
break break
if not finalized: if not finalized:
if self.config.log_bodies: if self.config.verbose:
log_json("model streaming assistant messages", accumulator.messages()) log_json("model streaming assistant messages", accumulator.messages())
stored = accumulator.store_reasoning(self.reasoning_store, scope) stored = accumulator.store_reasoning(self.reasoning_store, scope)
if stored: if stored:
@ -351,7 +369,7 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
data = stripped[len(b"data:") :].strip() data = stripped[len(b"data:") :].strip()
if data == b"[DONE]": if data == b"[DONE]":
if self.config.log_bodies: if self.config.verbose:
log_json("model streaming assistant messages", accumulator.messages()) log_json("model streaming assistant messages", accumulator.messages())
stored = accumulator.store_reasoning(self.reasoning_store, scope) stored = accumulator.store_reasoning(self.reasoning_store, scope)
if stored: if stored:
@ -425,12 +443,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument( parser.add_argument(
"--verbose", "--verbose",
action="store_true", action="store_true",
help="Log request lifecycle metadata without bodies", help="Log detailed request lifecycle metadata and full payloads",
)
parser.add_argument(
"--log-bodies",
action="store_true",
help="Log normalized upstream request bodies",
) )
parser.add_argument( parser.add_argument(
"--no-cursor-display-reasoning", "--no-cursor-display-reasoning",
@ -521,8 +534,6 @@ def main(argv: list[str] | None = None) -> int:
updates["ngrok"] = True updates["ngrok"] = True
if args.verbose: if args.verbose:
updates["verbose"] = True updates["verbose"] = True
if args.log_bodies:
updates["log_bodies"] = True
if args.no_cursor_display_reasoning: if args.no_cursor_display_reasoning:
updates["cursor_display_reasoning"] = False updates["cursor_display_reasoning"] = False
if updates: if updates:
@ -547,11 +558,12 @@ def main(argv: list[str] | None = None) -> int:
config.reasoning_content_path, config.reasoning_content_path,
) )
if config.verbose: if config.verbose:
LOG.info("verbose logging enabled") LOG.info("logging mode=verbose metadata=detailed bodies=true")
if config.log_bodies:
LOG.warning( 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 tunnel: NgrokTunnel | None = None
if config.ngrok: if config.ngrok:

View File

@ -135,18 +135,16 @@ class ConfigTests(unittest.TestCase):
home / ".deepseek-cursor-proxy" / "custom.sqlite3", 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( config = ProxyConfig.from_file(
env={ env={
"PROXY_VERBOSE": "true", "PROXY_VERBOSE": "true",
"PROXY_LOG_BODIES": "1",
"PROXY_NGROK": "yes", "PROXY_NGROK": "yes",
}, },
config_path=Path("/does/not/exist"), config_path=Path("/does/not/exist"),
) )
self.assertTrue(config.verbose) self.assertTrue(config.verbose)
self.assertTrue(config.log_bodies)
self.assertTrue(config.ngrok) self.assertTrue(config.ngrok)
def test_cursor_reasoning_display_can_be_disabled_from_config(self) -> None: def test_cursor_reasoning_display_can_be_disabled_from_config(self) -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import replace
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json import json
import threading import threading
@ -462,6 +463,40 @@ class ProxyEndToEndTests(unittest.TestCase):
self.assertEqual(status, 200) self.assertEqual(status, 200)
self.assertEqual(FakeDeepSeekHandler.auth_headers[0], "Bearer sk-from-cursor") 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: def test_proxy_rejects_missing_cursor_bearer_token(self) -> None:
request = Request( request = Request(
f"{self.proxy.url}/v1/chat/completions", f"{self.proxy.url}/v1/chat/completions",