Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions scripts/benchmark_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""Run a metadata-only DeepSeek streaming benchmark through one or more routes.

The API credential is read only from an environment variable and is never
printed. This deliberately measures one controlled completion, not Cursor's
agent orchestration; use the proxy timing log to correlate real agent turns.
"""

from __future__ import annotations

import argparse
import json
import os
import time
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def parse_target(value: str) -> tuple[str, str]:
name, separator, base_url = value.partition("=")
if not separator or not name or not base_url:
raise argparse.ArgumentTypeError("target must be NAME=https://host/v1")
return name, base_url.rstrip("/")


def parse_case(value: str) -> tuple[str, str]:
model, separator, effort = value.partition(":")
if not separator or not model or not effort:
raise argparse.ArgumentTypeError("case must be MODEL:EFFORT")
return model, effort


def parse_auth_env(value: str) -> tuple[str, str]:
target_name, separator, environment_name = value.partition("=")
if not separator or not target_name or not environment_name:
raise argparse.ArgumentTypeError("auth environment must be TARGET=ENV_VAR")
return target_name, environment_name


def parse_header_env(value: str) -> tuple[str, str, str]:
target_name, separator, header_and_environment = value.partition("=")
header_name, header_separator, environment_name = header_and_environment.partition(
":"
)
if (
not separator
or not target_name
or not header_separator
or not header_name
or not environment_name
):
raise argparse.ArgumentTypeError(
"header environment must be TARGET=HEADER_NAME:ENV_VAR"
)
return target_name, header_name, environment_name


def benchmark(
*,
base_url: str,
authorization: str,
model: str,
effort: str,
prompt: str,
timeout: float,
extra_headers: dict[str, str] | None = None,
) -> dict[str, Any]:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": True,
"stream_options": {"include_usage": True},
"thinking": {"type": "enabled"},
"reasoning_effort": effort,
}
request_body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
headers = {
"Authorization": authorization,
"Content-Type": "application/json",
"Accept": "text/event-stream",
# Some edge security configurations reject urllib's default user agent.
# Use a stable, identifiable value so every target is measured equally.
"User-Agent": "deepseek-cursor-proxy-benchmark/1.0",
}
if extra_headers:
headers.update(extra_headers)
request = Request(
f"{base_url}/chat/completions",
data=request_body,
method="POST",
headers=headers,
)
started = time.monotonic()
first_byte_ms: int | None = None
reasoning_chars = 0
visible_output_chars = 0
tool_call_count = 0
usage: dict[str, Any] | None = None
with urlopen(request, timeout=timeout) as response:
for raw_line in response:
if first_byte_ms is None and raw_line.strip():
first_byte_ms = round((time.monotonic() - started) * 1000)
if not raw_line.startswith(b"data:"):
continue
data = raw_line[5:].strip()
if data == b"[DONE]":
break
try:
chunk = json.loads(data)
except json.JSONDecodeError:
continue
if isinstance(chunk.get("usage"), dict):
usage = chunk["usage"]
for choice in chunk.get("choices") or []:
delta = choice.get("delta") if isinstance(choice, dict) else None
if not isinstance(delta, dict):
continue
reasoning = delta.get("reasoning_content")
content = delta.get("content")
reasoning_chars += len(reasoning) if isinstance(reasoning, str) else 0
visible_output_chars += len(content) if isinstance(content, str) else 0
tool_call_count += len(delta.get("tool_calls") or [])
return {
"model": model,
"effective_reasoning_effort": effort,
"request_body_bytes": len(request_body),
"time_to_first_upstream_byte_ms": first_byte_ms,
"total_response_ms": round((time.monotonic() - started) * 1000),
"reasoning_chars": reasoning_chars,
"visible_output_chars": visible_output_chars,
"tool_call_count": tool_call_count,
"usage": usage,
}


def benchmark_error(error: HTTPError | URLError) -> dict[str, Any]:
result: dict[str, Any] = {
"error_type": type(error).__name__,
"http_status": error.code if isinstance(error, HTTPError) else None,
}
if isinstance(error, HTTPError):
try:
body = json.loads(error.read().decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return result
message = body.get("error", {}).get("message") if isinstance(body, dict) else None
if isinstance(message, str):
# Error text is useful for route diagnostics, but never retain an
# arbitrary large server response in a benchmark result.
result["error_message"] = message[:200]
return result


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--target",
action="append",
type=parse_target,
required=True,
help="repeatable NAME=https://host/v1 target",
)
parser.add_argument(
"--case",
action="append",
type=parse_case,
default=[
("deepseek-v4-flash", "max"),
("deepseek-v4-flash", "high"),
("deepseek-v4-pro", "high"),
],
help="repeatable MODEL:EFFORT case",
)
parser.add_argument("--api-key-env", default="DEEPSEEK_BENCHMARK_API_KEY")
parser.add_argument(
"--auth-env",
action="append",
type=parse_auth_env,
default=[],
metavar="TARGET=ENV_VAR",
help=(
"override the credential environment variable for a target; use this "
"for the Cloudflare route, which accepts PROXY_BEARER_TOKEN rather "
"than a DeepSeek API key"
),
)
parser.add_argument(
"--header-env",
action="append",
type=parse_header_env,
default=[],
metavar="TARGET=HEADER_NAME:ENV_VAR",
help="add a target-specific request header from an environment variable",
)
parser.add_argument("--timeout", type=float, default=180.0)
parser.add_argument(
"--prompt",
default="Propose a concise, safe plan for a one-file configuration change.",
)
args = parser.parse_args()
target_auth_env = dict(args.auth_env)
target_header_env: dict[str, list[tuple[str, str]]] = {}
for target_name, header_name, environment_name in args.header_env:
target_header_env.setdefault(target_name, []).append(
(header_name, environment_name)
)
for target_name, base_url in args.target:
auth_env = target_auth_env.get(target_name, args.api_key_env)
api_key = os.environ.get(auth_env)
if not api_key:
parser.error(
f"set {auth_env} for target {target_name}; its value is never printed"
)
authorization = (
api_key
if api_key.lower().startswith("bearer ")
else f"Bearer {api_key}"
)
extra_headers: dict[str, str] = {}
for header_name, environment_name in target_header_env.get(target_name, []):
value = os.environ.get(environment_name)
if not value:
parser.error(
f"set {environment_name} for header {header_name} on target "
f"{target_name}; its value is never printed"
)
extra_headers[header_name] = value
for model, effort in args.case:
try:
result = benchmark(
base_url=base_url,
authorization=authorization,
model=model,
effort=effort,
prompt=args.prompt,
timeout=args.timeout,
extra_headers=extra_headers,
)
except (HTTPError, URLError) as error:
result = {
"model": model,
"effective_reasoning_effort": effort,
**benchmark_error(error),
}
result["target"] = target_name
print(json.dumps(result, sort_keys=True))
return 0


if __name__ == "__main__":
raise SystemExit(main())
4 changes: 2 additions & 2 deletions src/deepseek_cursor_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
DEFAULT_UPSTREAM_BASE_URL = "https://api.deepseek.com"
DEFAULT_UPSTREAM_MODEL = "deepseek-v4-pro"
DEFAULT_THINKING = "enabled"
DEFAULT_REASONING_EFFORT = "max"
DEFAULT_DISPLAY_REASONING = True
DEFAULT_REASONING_EFFORT = "high"
DEFAULT_DISPLAY_REASONING = False
DEFAULT_COLLAPSIBLE_REASONING = True
DEFAULT_NGROK = True
DEFAULT_VERBOSE = False
Expand Down
32 changes: 21 additions & 11 deletions src/deepseek_cursor_proxy/reasoning_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import hashlib
import json
from pathlib import Path
import sqlite3
import threading
import time
from collections.abc import Iterable
from pathlib import Path
from typing import Any


Expand Down Expand Up @@ -217,11 +218,24 @@ def close(self) -> None:
self._conn.close()

def put(self, key: str, reasoning: str, message: dict[str, Any]) -> None:
self.put_many((key,), reasoning, message)

def put_many(
self,
keys: Iterable[str],
reasoning: str,
message: dict[str, Any],
) -> int:
"""Store all aliases for a message in one SQLite transaction."""
if not isinstance(reasoning, str):
return
return 0
unique_keys = list(dict.fromkeys(key for key in keys if isinstance(key, str)))
if not unique_keys:
return 0
message_json = json.dumps(message, ensure_ascii=False, sort_keys=True)
now = time.time()
with self._lock:
self._conn.execute(
self._conn.executemany(
"""
INSERT INTO reasoning_cache(key, reasoning, message_json, created_at)
VALUES (?, ?, ?, ?)
Expand All @@ -230,10 +244,11 @@ def put(self, key: str, reasoning: str, message: dict[str, Any]) -> None:
message_json = excluded.message_json,
created_at = excluded.created_at
""",
(key, reasoning, message_json, time.time()),
((key, reasoning, message_json, now) for key in unique_keys),
)
self._prune_locked()
self._conn.commit()
return len(unique_keys)

def get(self, key: str) -> str | None:
with self._lock:
Expand Down Expand Up @@ -263,10 +278,7 @@ def store_assistant_message(
keys.extend(
portable_reasoning_keys(message, cache_namespace, prior_messages)
)
keys = list(dict.fromkeys(keys))
for key in keys:
self.put(key, reasoning, message)
return len(keys)
return self.put_many(keys, reasoning, message)

def lookup_for_message(
self,
Expand Down Expand Up @@ -300,9 +312,7 @@ def backfill_portable_aliases(
return 0
message_with_reasoning = dict(message)
message_with_reasoning["reasoning_content"] = reasoning
for key in dict.fromkeys(keys):
self.put(key, reasoning, message_with_reasoning)
return len(keys)
return self.put_many(keys, reasoning, message_with_reasoning)

def clear(self) -> int:
with self._lock:
Expand Down
Loading
Loading