refactor(logging): merge log_bodies into verbose mode and improve log levels (#4)
parent
c238a40045
commit
08eeb87c48
|
|
@ -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
|
||||
```
|
||||
17
README.md
17
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue