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:
```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.

View File

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

View File

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

View File

@ -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,13 +103,15 @@ 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))
prepared = prepare_upstream_request(payload, self.config, self.reasoning_store)
@ -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,16 +161,15 @@ 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",
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),
@ -175,10 +181,11 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
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:

View File

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

View File

@ -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",