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:
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,14 +103,16 @@ 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)
|
||||||
if prepared.patched_reasoning_messages:
|
if prepared.patched_reasoning_messages:
|
||||||
|
|
@ -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,31 +161,31 @@ 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),
|
exc.reason,
|
||||||
exc.reason,
|
)
|
||||||
)
|
|
||||||
self._send_json(
|
self._send_json(
|
||||||
502, {"error": {"message": f"Upstream request failed: {exc.reason}"}}
|
502, {"error": {"message": f"Upstream request failed: {exc.reason}"}}
|
||||||
)
|
)
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue