refactor(config): migrate to yaml and forward auth (#3)
parent
1717331057
commit
c238a40045
22
.env.example
22
.env.example
|
|
@ -1,22 +0,0 @@
|
||||||
# Copy this file to ~/.deepseek-cursor-proxy/.env.
|
|
||||||
# The proxy loads that file automatically and keeps secrets out of the repo.
|
|
||||||
|
|
||||||
DEEPSEEK_API_KEY=sk-your-deepseek-key
|
|
||||||
|
|
||||||
# Use this as the OpenAI API key in Cursor.
|
|
||||||
PROXY_API_KEY=cursor-local-token
|
|
||||||
|
|
||||||
DEEPSEEK_MODEL=deepseek-v4-pro
|
|
||||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
|
||||||
DEEPSEEK_THINKING=enabled
|
|
||||||
DEEPSEEK_REASONING_EFFORT=high
|
|
||||||
CURSOR_DISPLAY_REASONING=true
|
|
||||||
|
|
||||||
PROXY_HOST=127.0.0.1
|
|
||||||
PROXY_PORT=9000
|
|
||||||
PROXY_NGROK=true
|
|
||||||
PROXY_VERBOSE=false
|
|
||||||
PROXY_LOG_BODIES=false
|
|
||||||
|
|
||||||
# Optional. Default: ~/.deepseek-cursor-proxy/reasoning_content.sqlite3
|
|
||||||
REASONING_CONTENT_PATH=~/.deepseek-cursor-proxy/reasoning_content.sqlite3
|
|
||||||
152
README.md
152
README.md
|
|
@ -1,98 +1,91 @@
|
||||||
# deepseek-cursor-proxy
|
# deepseek-cursor-proxy
|
||||||
|
|
||||||
A simple proxy that caches and restores DeepSeek `reasoning_content` across tool-call turns in Cursor, making thinking models like `deepseek-v4-pro` and `deepseek-v4-flash` work correctly.
|
Compatibility proxy connecting Cursor to DeepSeek thinking models (`deepseek-v4-pro` and `deepseek-v4-flash`).
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
- Caches DeepSeek `reasoning_content` from regular and streamed responses, then restores it on later tool-call turns when Cursor omits it.
|
- ✅ Caches DeepSeek `reasoning_content` from regular and streamed responses, then restores it on later tool-call turns when Cursor omits it. See [DeepSeek docs](https://api-docs.deepseek.com/guides/thinking_mode#tool-calls) for more details.
|
||||||
- Mirrors streamed `reasoning_content` into Cursor-visible `<think>...</think>` text so thinking tokens are shown in Cursor BYOK/proxy chats. Cursor currently renders this as normal chat text, not as a native collapsible Thinking block.
|
- ✅ Mirrors streamed `reasoning_content` into Cursor-visible `<think>...</think>` text so that thinking tokens are shown in Cursor's UI. For BYOK/proxy mode, Cursor renders this as normal text, not as a native collapsible thinking block.
|
||||||
- Provides other compatibility fixes for running Cursor with the DeepSeek official API.
|
- ✅ Starts an ngrok tunnel so Cursor can reach the local proxy.
|
||||||
|
- ✅ Provides other compatibility fixes to make DeepSeek models run well in Cursor.
|
||||||
|
|
||||||
## Why This Exists
|
## Why This Exists
|
||||||
|
|
||||||
DeepSeek thinking mode returns `reasoning_content` separately from final `content`. After an assistant turn with tool calls, DeepSeek requires that same `reasoning_content` to be sent back in later requests. Cursor can omit it in custom OpenAI-compatible flows, causing `The reasoning_content in the thinking mode must be passed back to the API.` This proxy caches reasoning by conversation prefix, message signature, and tool-call IDs, then restores it before forwarding to DeepSeek.
|
This repository fixes the following Cursor + DeepSeek tool-call error with thinking mode enabled:
|
||||||
|
|
||||||
For streamed responses, the proxy also mirrors DeepSeek `reasoning_content` into Cursor-visible `<think>...</think>` content while leaving the original `reasoning_content` field intact. This lets Cursor display the thinking text in OpenAI-compatible BYOK/proxy flows, and the proxy strips those display-only tags from later assistant history before replaying it to DeepSeek.
|
|
||||||
|
|
||||||
This repo fixes the following error:
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
⚠️ Connection Error
|
⚠️ Connection Error
|
||||||
|
Provider returned error:
|
||||||
Provider returned error: {"error":{"message":"The reasoning_content in the thinking mode must be passed back to the
|
{
|
||||||
API.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}
|
"error": {
|
||||||
|
"message": "The reasoning_content in the thinking mode must be passed back to the API.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "invalid_request_error"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 1. Install
|
## Usage
|
||||||
|
|
||||||
```bash
|
### Step 1: Set Up ngrok
|
||||||
source ~/miniconda3/etc/profile.d/conda.sh
|
|
||||||
conda activate pytools
|
|
||||||
PIP_REQUIRE_VIRTUALENV=false python -m pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Configure
|
Create an ngrok account, visit ngrok's Dashboard: https://dashboard.ngrok.com
|
||||||
|
|
||||||
```bash
|

|
||||||
mkdir -p ~/.deepseek-cursor-proxy
|
|
||||||
chmod 700 ~/.deepseek-cursor-proxy
|
|
||||||
cp .env.example ~/.deepseek-cursor-proxy/.env
|
|
||||||
chmod 600 ~/.deepseek-cursor-proxy/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
`.env.example` is only a safe template. The proxy loads `~/.deepseek-cursor-proxy/.env` automatically, and that file should stay outside this repository because it contains your keys.
|
Then, install and authenticate ngrok once:
|
||||||
|
|
||||||
Edit `~/.deepseek-cursor-proxy/.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DEEPSEEK_API_KEY=sk-your-deepseek-key
|
|
||||||
PROXY_API_KEY=cursor-local-token
|
|
||||||
CURSOR_DISPLAY_REASONING=true
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep `PROXY_API_KEY` set when using ngrok because the proxy will be reachable from the public internet.
|
|
||||||
|
|
||||||
By default, reasoning cache data is stored at:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/.deepseek-cursor-proxy/reasoning_content.sqlite3
|
|
||||||
```
|
|
||||||
|
|
||||||
Override it with `REASONING_CONTENT_PATH` or `deepseek-cursor-proxy --reasoning-content-path <path>` only when you need a custom location.
|
|
||||||
|
|
||||||
## 3. Set Up Ngrok Once
|
|
||||||
|
|
||||||
- Create/login to an ngrok account: https://dashboard.ngrok.com/signup
|
|
||||||
- Copy your authtoken from the dashboard: https://dashboard.ngrok.com/get-started/your-authtoken
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install ngrok
|
brew install ngrok
|
||||||
ngrok config add-authtoken <your-ngrok-token>
|
ngrok config add-authtoken <your-ngrok-token>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Run
|
### Step 2: Add Cursor Custom Model
|
||||||
|
|
||||||
|
In Cursor, add the DeepSeek custom model and point it at this proxy:
|
||||||
|
|
||||||
|
- Model: `deepseek-v4-pro`
|
||||||
|
- API Key: your DeepSeek API key
|
||||||
|
- Base URL: your ngrok HTTPS URL with the `/v1` API version path
|
||||||
|
|
||||||
|
For example, if ngrok dashboard shows `https://example.ngrok-free.app`, use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://example.ngrok-free.app/v1
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Note: you can toggle the custom API on and off with:
|
||||||
|
|
||||||
|
- macOS: `Cmd+Shift+0`
|
||||||
|
- Windows/Linux: `Ctrl+Shift+0`
|
||||||
|
|
||||||
|
### Step 3: Start the Proxy Server
|
||||||
|
|
||||||
|
Install and run the proxy:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
conda create -n dcp python=3.10 -y
|
||||||
|
conda activate dcp
|
||||||
|
pip install -e .
|
||||||
deepseek-cursor-proxy --verbose
|
deepseek-cursor-proxy --verbose
|
||||||
```
|
```
|
||||||
|
|
||||||
The proxy prints a line like:
|
The proxy creates `~/.deepseek-cursor-proxy/config.yaml` on first run.
|
||||||
|
|
||||||
```text
|
This will also print the ngrok public URL. If it differs from the one in Cursor, update it in Cursor's Base URL field.
|
||||||
Cursor Base URL: https://example.ngrok-free.app/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
Use that URL in Cursor. If you do not use ngrok and point Cursor at `localhost` or `127.0.0.1`, Cursor may fail with `ssrf_blocked: connection to private IP is blocked`.
|
### Step 4: Chat with DeepSeek in Cursor
|
||||||
|
|
||||||
## 5. Cursor Settings
|
Select `deepseek-v4-pro` in Cursor and use chat or agent mode as usual.
|
||||||
|
|
||||||
- OpenAI Base URL: the printed ngrok URL ending in `/v1`
|

|
||||||
- OpenAI API Key: the value of `PROXY_API_KEY`
|
|
||||||
- Model: `deepseek-v4-pro`
|
|
||||||
|
|
||||||
## Useful Commands
|
## Debugging and Development
|
||||||
|
|
||||||
Run without ngrok for local curl testing:
|
Run without ngrok for local curl testing:
|
||||||
|
|
||||||
|
|
@ -100,24 +93,10 @@ Run without ngrok for local curl testing:
|
||||||
PROXY_NGROK=false deepseek-cursor-proxy --port 9000 --verbose
|
PROXY_NGROK=false deepseek-cursor-proxy --port 9000 --verbose
|
||||||
```
|
```
|
||||||
|
|
||||||
Disable the Cursor display mirror if you only want raw OpenAI-compatible response fields:
|
Use another config file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CURSOR_DISPLAY_REASONING=false deepseek-cursor-proxy --verbose
|
deepseek-cursor-proxy --config ./dev.config.yaml
|
||||||
```
|
|
||||||
|
|
||||||
Log full request bodies only when needed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
deepseek-cursor-proxy --ngrok --verbose --log-bodies
|
|
||||||
```
|
|
||||||
|
|
||||||
This prints the Cursor request body, the normalized DeepSeek request body, DeepSeek error bodies, and the final streamed assistant message.
|
|
||||||
|
|
||||||
Use a different env file for development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
deepseek-cursor-proxy --config ./dev.env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Run tests:
|
Run tests:
|
||||||
|
|
@ -125,24 +104,3 @@ Run tests:
|
||||||
```bash
|
```bash
|
||||||
PYTHONPATH=src python -m unittest discover -s tests
|
PYTHONPATH=src python -m unittest discover -s tests
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Pre-commit runs whitespace checks, Black, and Ruff:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PIP_REQUIRE_VIRTUALENV=false python -m pip install -e ".[dev]"
|
|
||||||
pre-commit install
|
|
||||||
pre-commit run --all-files
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Distribution name: `deepseek-cursor-proxy`
|
|
||||||
- Import package: `deepseek_cursor_proxy`
|
|
||||||
- User config file: `~/.deepseek-cursor-proxy/.env`
|
|
||||||
- Cache file: `~/.deepseek-cursor-proxy/reasoning_content.sqlite3`
|
|
||||||
- DeepSeek thinking docs: https://api-docs.deepseek.com/guides/thinking_mode
|
|
||||||
- DeepSeek chat completion docs: https://api-docs.deepseek.com/api/create-chat-completion
|
|
||||||
- Cursor forum report: https://forum.cursor.com/t/compatibility-with-deepseek-models-design-to-return-reasoning-content-after-tool-calls/158905
|
|
||||||
- ngrok setup docs: https://ngrok.com/downloads
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -0,0 +1,17 @@
|
||||||
|
# This file was created automatically at ~/.deepseek-cursor-proxy/config.yaml.
|
||||||
|
# API keys are read from Cursor's Authorization header and forwarded upstream.
|
||||||
|
|
||||||
|
base_url: https://api.deepseek.com
|
||||||
|
model: deepseek-v4-pro
|
||||||
|
thinking: enabled
|
||||||
|
reasoning_effort: high
|
||||||
|
display_reasoning: true
|
||||||
|
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 9000
|
||||||
|
ngrok: true
|
||||||
|
verbose: false
|
||||||
|
log_bodies: false
|
||||||
|
request_timeout: 300
|
||||||
|
|
||||||
|
reasoning_content_path: reasoning_content.sqlite3
|
||||||
|
|
@ -25,7 +25,9 @@ classifiers = [
|
||||||
"Topic :: Internet :: Proxy Servers",
|
"Topic :: Internet :: Proxy Servers",
|
||||||
"Topic :: Software Development",
|
"Topic :: Software Development",
|
||||||
]
|
]
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"PyYAML>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,36 @@ from __future__ import annotations
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
APP_DIR_NAME = ".deepseek-cursor-proxy"
|
APP_DIR_NAME = ".deepseek-cursor-proxy"
|
||||||
CONFIG_FILE_NAME = ".env"
|
CONFIG_FILE_NAME = "config.yaml"
|
||||||
REASONING_CONTENT_FILE_NAME = "reasoning_content.sqlite3"
|
REASONING_CONTENT_FILE_NAME = "reasoning_content.sqlite3"
|
||||||
|
|
||||||
TRUE_VALUES = {"1", "true", "yes", "on"}
|
TRUE_VALUES = {"1", "true", "yes", "on"}
|
||||||
FALSE_VALUES = {"0", "false", "no", "off"}
|
FALSE_VALUES = {"0", "false", "no", "off"}
|
||||||
|
MISSING = object()
|
||||||
|
DEFAULT_CONFIG_TEXT = """# This file was created automatically at ~/.deepseek-cursor-proxy/config.yaml.
|
||||||
|
# API keys are read from Cursor's Authorization header and forwarded upstream.
|
||||||
|
|
||||||
|
base_url: https://api.deepseek.com
|
||||||
|
model: deepseek-v4-pro
|
||||||
|
thinking: enabled
|
||||||
|
reasoning_effort: high
|
||||||
|
display_reasoning: true
|
||||||
|
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 9000
|
||||||
|
ngrok: true
|
||||||
|
verbose: false
|
||||||
|
log_bodies: false
|
||||||
|
request_timeout: 300
|
||||||
|
|
||||||
|
reasoning_content_path: reasoning_content.sqlite3
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def default_app_dir() -> Path:
|
def default_app_dir() -> Path:
|
||||||
|
|
@ -26,45 +47,65 @@ def default_reasoning_content_path() -> Path:
|
||||||
return default_app_dir() / REASONING_CONTENT_FILE_NAME
|
return default_app_dir() / REASONING_CONTENT_FILE_NAME
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(env_file_path: str | Path) -> dict[str, str]:
|
def populate_default_config_file(config_path: Path) -> None:
|
||||||
env_file_path = Path(env_file_path)
|
config_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
if not env_file_path.exists():
|
config_path.parent.chmod(0o700)
|
||||||
|
config_path.write_text(DEFAULT_CONFIG_TEXT, encoding="utf-8")
|
||||||
|
config_path.chmod(0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_file(config_path: str | Path) -> dict[str, Any]:
|
||||||
|
config_path = Path(config_path).expanduser()
|
||||||
|
if not config_path.exists():
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
values: dict[str, str] = {}
|
try:
|
||||||
for raw_line in env_file_path.read_text(encoding="utf-8").splitlines():
|
loaded = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||||
line = raw_line.strip()
|
except yaml.YAMLError as exc:
|
||||||
if not line or line.startswith("#") or "=" not in line:
|
raise ValueError(f"Invalid YAML config at {config_path}: {exc}") from exc
|
||||||
continue
|
if loaded is None:
|
||||||
if line.startswith("export "):
|
return {}
|
||||||
line = line.removeprefix("export ").strip()
|
if not isinstance(loaded, Mapping):
|
||||||
key, value = line.split("=", 1)
|
raise ValueError(f"Config file must contain a YAML mapping: {config_path}")
|
||||||
key = key.strip()
|
return dict(loaded)
|
||||||
value = value.strip().strip('"').strip("'")
|
|
||||||
if key:
|
|
||||||
values[key] = value
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
def merged_env(
|
def resolve_config_path(
|
||||||
env: Mapping[str, str] | None, env_file_path: str | Path | None
|
env: Mapping[str, str] | None, config_path: str | Path | None
|
||||||
) -> dict[str, str]:
|
) -> Path:
|
||||||
live_env = dict(os.environ if env is None else env)
|
live_env = os.environ if env is None else env
|
||||||
config_path = Path(
|
return Path(
|
||||||
env_file_path
|
config_path
|
||||||
or live_env.get("DEEPSEEK_CURSOR_PROXY_CONFIG_PATH")
|
or live_env.get("DEEPSEEK_CURSOR_PROXY_CONFIG_PATH")
|
||||||
or default_config_path()
|
or default_config_path()
|
||||||
)
|
).expanduser()
|
||||||
values = load_env_file(config_path)
|
|
||||||
values.update(live_env)
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
def env_bool(values: Mapping[str, str], name: str, default: bool) -> bool:
|
def setting_value(
|
||||||
value = values.get(name)
|
settings: Mapping[str, Any],
|
||||||
if value is None:
|
env: Mapping[str, str],
|
||||||
|
key: str,
|
||||||
|
env_name: str,
|
||||||
|
) -> Any:
|
||||||
|
if env_name in env:
|
||||||
|
return env[env_name]
|
||||||
|
return settings.get(key, MISSING)
|
||||||
|
|
||||||
|
|
||||||
|
def as_str(value: Any, default: str) -> str:
|
||||||
|
if value is MISSING or value is None:
|
||||||
return default
|
return default
|
||||||
normalized = value.strip().lower()
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def as_bool(value: Any, default: bool) -> bool:
|
||||||
|
if value is MISSING or value is None:
|
||||||
|
return default
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, int):
|
||||||
|
return bool(value)
|
||||||
|
normalized = str(value).strip().lower()
|
||||||
if normalized in TRUE_VALUES:
|
if normalized in TRUE_VALUES:
|
||||||
return True
|
return True
|
||||||
if normalized in FALSE_VALUES:
|
if normalized in FALSE_VALUES:
|
||||||
|
|
@ -72,48 +113,45 @@ def env_bool(values: Mapping[str, str], name: str, default: bool) -> bool:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def env_int(values: Mapping[str, str], name: str, default: int) -> int:
|
def as_int(value: Any, default: int) -> int:
|
||||||
value = values.get(name)
|
if value is MISSING or value is None:
|
||||||
if value is None:
|
|
||||||
return default
|
return default
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except ValueError:
|
except (TypeError, ValueError):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def env_float(values: Mapping[str, str], name: str, default: float) -> float:
|
def as_float(value: Any, default: float) -> float:
|
||||||
value = values.get(name)
|
if value is MISSING or value is None:
|
||||||
if value is None:
|
|
||||||
return default
|
return default
|
||||||
try:
|
try:
|
||||||
return float(value)
|
return float(value)
|
||||||
except ValueError:
|
except (TypeError, ValueError):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def env_tuple(
|
def as_path(value: Any, default_path: Path, relative_base: Path) -> Path:
|
||||||
values: Mapping[str, str], name: str, default: tuple[str, ...]
|
if value is MISSING or value is None or value == "":
|
||||||
) -> tuple[str, ...]:
|
return default_path
|
||||||
value = values.get(name)
|
candidate_path = Path(str(value)).expanduser()
|
||||||
if not value:
|
if candidate_path.is_absolute():
|
||||||
return default
|
return candidate_path
|
||||||
return tuple(item.strip() for item in value.split(",") if item.strip())
|
return relative_base / candidate_path
|
||||||
|
|
||||||
|
|
||||||
def env_path(
|
def settings_and_env(
|
||||||
values: Mapping[str, str],
|
env: Mapping[str, str] | None, config_path: str | Path | None
|
||||||
names: tuple[str, ...],
|
) -> tuple[dict[str, Any], dict[str, str], Path]:
|
||||||
default_path: Path,
|
live_env = dict(os.environ if env is None else env)
|
||||||
) -> Path:
|
config_path = resolve_config_path(live_env, config_path)
|
||||||
for env_name in names:
|
if (
|
||||||
value = values.get(env_name)
|
config_path == default_config_path()
|
||||||
if value:
|
and "DEEPSEEK_CURSOR_PROXY_CONFIG_PATH" not in live_env
|
||||||
candidate_path = Path(value).expanduser()
|
and not config_path.exists()
|
||||||
if candidate_path.is_absolute():
|
):
|
||||||
return candidate_path
|
populate_default_config_file(config_path)
|
||||||
return default_path.parent / candidate_path
|
return load_config_file(config_path), live_env, config_path
|
||||||
return default_path
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -121,8 +159,6 @@ class ProxyConfig:
|
||||||
host: str = "127.0.0.1"
|
host: str = "127.0.0.1"
|
||||||
port: int = 9000
|
port: int = 9000
|
||||||
upstream_base_url: str = "https://api.deepseek.com"
|
upstream_base_url: str = "https://api.deepseek.com"
|
||||||
upstream_api_key: str = ""
|
|
||||||
proxy_api_key: str | None = None
|
|
||||||
upstream_model: str = "deepseek-v4-pro"
|
upstream_model: str = "deepseek-v4-pro"
|
||||||
allow_model_passthrough: bool = False
|
allow_model_passthrough: bool = False
|
||||||
thinking: str = "enabled"
|
thinking: str = "enabled"
|
||||||
|
|
@ -133,52 +169,143 @@ class ProxyConfig:
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
log_bodies: bool = False
|
log_bodies: bool = False
|
||||||
ngrok: bool = False
|
ngrok: bool = False
|
||||||
model_list: tuple[str, ...] = ("deepseek-v4-pro", "deepseek-v4-flash")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(
|
def from_file(
|
||||||
cls: type[ProxyConfig],
|
cls: type[ProxyConfig],
|
||||||
env: Mapping[str, str] | None = None,
|
env: Mapping[str, str] | None = None,
|
||||||
env_file_path: str | Path | None = None,
|
config_path: str | Path | None = None,
|
||||||
) -> "ProxyConfig":
|
) -> "ProxyConfig":
|
||||||
values = merged_env(env, env_file_path)
|
settings, live_env, resolved_config_path = settings_and_env(env, config_path)
|
||||||
thinking = values.get("DEEPSEEK_THINKING", "enabled").strip().lower()
|
config_dir = resolved_config_path.parent
|
||||||
|
|
||||||
|
thinking = (
|
||||||
|
as_str(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"thinking",
|
||||||
|
"DEEPSEEK_THINKING",
|
||||||
|
),
|
||||||
|
"enabled",
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
if thinking in {"passthrough", "pass-through", "pass_through"}:
|
if thinking in {"passthrough", "pass-through", "pass_through"}:
|
||||||
thinking = "pass-through"
|
thinking = "pass-through"
|
||||||
if thinking not in {"enabled", "disabled", "pass-through"}:
|
if thinking not in {"enabled", "disabled", "pass-through"}:
|
||||||
thinking = "enabled"
|
thinking = "enabled"
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
host=values.get("PROXY_HOST", "127.0.0.1"),
|
host=as_str(
|
||||||
port=env_int(values, "PROXY_PORT", 9000),
|
setting_value(
|
||||||
upstream_base_url=values.get(
|
settings,
|
||||||
"DEEPSEEK_BASE_URL", "https://api.deepseek.com"
|
live_env,
|
||||||
|
"host",
|
||||||
|
"PROXY_HOST",
|
||||||
|
),
|
||||||
|
"127.0.0.1",
|
||||||
|
),
|
||||||
|
port=as_int(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"port",
|
||||||
|
"PROXY_PORT",
|
||||||
|
),
|
||||||
|
9000,
|
||||||
|
),
|
||||||
|
upstream_base_url=as_str(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"base_url",
|
||||||
|
"DEEPSEEK_BASE_URL",
|
||||||
|
),
|
||||||
|
"https://api.deepseek.com",
|
||||||
).rstrip("/"),
|
).rstrip("/"),
|
||||||
upstream_api_key=values.get("DEEPSEEK_API_KEY", ""),
|
upstream_model=as_str(
|
||||||
proxy_api_key=values.get("PROXY_API_KEY") or None,
|
setting_value(
|
||||||
upstream_model=values.get("DEEPSEEK_MODEL", "deepseek-v4-pro"),
|
settings,
|
||||||
allow_model_passthrough=env_bool(
|
live_env,
|
||||||
values, "DEEPSEEK_ALLOW_MODEL_PASSTHROUGH", False
|
"model",
|
||||||
|
"DEEPSEEK_MODEL",
|
||||||
|
),
|
||||||
|
"deepseek-v4-pro",
|
||||||
|
),
|
||||||
|
allow_model_passthrough=as_bool(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"allow_model_passthrough",
|
||||||
|
"DEEPSEEK_ALLOW_MODEL_PASSTHROUGH",
|
||||||
|
),
|
||||||
|
False,
|
||||||
),
|
),
|
||||||
thinking=thinking,
|
thinking=thinking,
|
||||||
reasoning_effort=values.get("DEEPSEEK_REASONING_EFFORT", "high"),
|
reasoning_effort=as_str(
|
||||||
request_timeout=env_float(values, "PROXY_REQUEST_TIMEOUT", 300.0),
|
setting_value(
|
||||||
reasoning_content_path=env_path(
|
settings,
|
||||||
values,
|
live_env,
|
||||||
("REASONING_CONTENT_PATH",),
|
"reasoning_effort",
|
||||||
default_reasoning_content_path(),
|
"DEEPSEEK_REASONING_EFFORT",
|
||||||
|
),
|
||||||
|
"high",
|
||||||
),
|
),
|
||||||
cursor_display_reasoning=env_bool(values, "CURSOR_DISPLAY_REASONING", True),
|
request_timeout=as_float(
|
||||||
verbose=env_bool(values, "PROXY_VERBOSE", False),
|
setting_value(
|
||||||
log_bodies=env_bool(values, "PROXY_LOG_BODIES", False),
|
settings,
|
||||||
ngrok=env_bool(values, "PROXY_NGROK", False),
|
live_env,
|
||||||
model_list=env_tuple(
|
"request_timeout",
|
||||||
values,
|
"PROXY_REQUEST_TIMEOUT",
|
||||||
"PROXY_MODELS",
|
),
|
||||||
("deepseek-v4-pro", "deepseek-v4-flash"),
|
300.0,
|
||||||
|
),
|
||||||
|
reasoning_content_path=as_path(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"reasoning_content_path",
|
||||||
|
"REASONING_CONTENT_PATH",
|
||||||
|
),
|
||||||
|
default_reasoning_content_path(),
|
||||||
|
config_dir,
|
||||||
|
),
|
||||||
|
cursor_display_reasoning=as_bool(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"display_reasoning",
|
||||||
|
"CURSOR_DISPLAY_REASONING",
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
verbose=as_bool(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"verbose",
|
||||||
|
"PROXY_VERBOSE",
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
log_bodies=as_bool(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"log_bodies",
|
||||||
|
"PROXY_LOG_BODIES",
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
ngrok=as_bool(
|
||||||
|
setting_value(
|
||||||
|
settings,
|
||||||
|
live_env,
|
||||||
|
"ngrok",
|
||||||
|
"PROXY_NGROK",
|
||||||
|
),
|
||||||
|
False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self) -> None:
|
|
||||||
if not self.upstream_api_key:
|
|
||||||
raise ValueError("DEEPSEEK_API_KEY is required")
|
|
||||||
|
|
|
||||||
|
|
@ -88,18 +88,13 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
|
||||||
404, {"error": {"message": "Only /v1/chat/completions is supported"}}
|
404, {"error": {"message": "Only /v1/chat/completions is supported"}}
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not self._authorized():
|
cursor_authorization = self._cursor_authorization()
|
||||||
|
if cursor_authorization is None:
|
||||||
self._send_json(
|
self._send_json(
|
||||||
401, {"error": {"message": "Missing or invalid proxy API key"}}
|
401, {"error": {"message": "Missing Authorization bearer token"}}
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
self.config.validate()
|
|
||||||
except ValueError as exc:
|
|
||||||
self._send_json(500, {"error": {"message": str(exc)}})
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = self._read_json_body()
|
payload = self._read_json_body()
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|
@ -148,7 +143,10 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
|
||||||
upstream_url,
|
upstream_url,
|
||||||
data=upstream_body,
|
data=upstream_body,
|
||||||
method="POST",
|
method="POST",
|
||||||
headers=self._upstream_headers(stream=bool(prepared.payload.get("stream"))),
|
headers=self._upstream_headers(
|
||||||
|
stream=bool(prepared.payload.get("stream")),
|
||||||
|
authorization=cursor_authorization,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -193,12 +191,12 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
|
||||||
response, prepared.original_model, prepared.payload["messages"]
|
response, prepared.original_model, prepared.payload["messages"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _authorized(self) -> bool:
|
def _cursor_authorization(self) -> str | None:
|
||||||
expected = self.config.proxy_api_key
|
|
||||||
if expected is None:
|
|
||||||
return True
|
|
||||||
auth_header = self.headers.get("Authorization", "")
|
auth_header = self.headers.get("Authorization", "")
|
||||||
return auth_header == f"Bearer {expected}"
|
scheme, separator, token = auth_header.strip().partition(" ")
|
||||||
|
if separator != " " or scheme.lower() != "bearer" or not token.strip():
|
||||||
|
return None
|
||||||
|
return f"Bearer {token.strip()}"
|
||||||
|
|
||||||
def _send_cors_headers(self) -> None:
|
def _send_cors_headers(self) -> None:
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
|
@ -223,20 +221,14 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
def _send_models(self) -> None:
|
def _send_models(self) -> None:
|
||||||
created = int(time.time())
|
created = int(time.time())
|
||||||
seen: set[str] = set()
|
models = [
|
||||||
models = []
|
{
|
||||||
for model_id in (self.config.upstream_model, *self.config.model_list):
|
"id": self.config.upstream_model,
|
||||||
if model_id in seen:
|
"object": "model",
|
||||||
continue
|
"created": created,
|
||||||
seen.add(model_id)
|
"owned_by": "deepseek",
|
||||||
models.append(
|
}
|
||||||
{
|
]
|
||||||
"id": model_id,
|
|
||||||
"object": "model",
|
|
||||||
"created": created,
|
|
||||||
"owned_by": "deepseek",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self._send_json(200, {"object": "list", "data": models})
|
self._send_json(200, {"object": "list", "data": models})
|
||||||
|
|
||||||
def _read_json_body(self) -> dict[str, Any]:
|
def _read_json_body(self) -> dict[str, Any]:
|
||||||
|
|
@ -252,9 +244,9 @@ class DeepSeekProxyHandler(BaseHTTPRequestHandler):
|
||||||
raise ValueError("Request body must be a JSON object")
|
raise ValueError("Request body must be a JSON object")
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def _upstream_headers(self, stream: bool) -> dict[str, str]:
|
def _upstream_headers(self, stream: bool, authorization: str) -> dict[str, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.config.upstream_api_key}",
|
"Authorization": authorization,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "text/event-stream" if stream else "application/json",
|
"Accept": "text/event-stream" if stream else "application/json",
|
||||||
"Accept-Encoding": "identity",
|
"Accept-Encoding": "identity",
|
||||||
|
|
@ -399,19 +391,23 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||||
"--config",
|
"--config",
|
||||||
dest="config_path",
|
dest="config_path",
|
||||||
type=Path,
|
type=Path,
|
||||||
help=f"Env config file, default {default_config_path()}",
|
help=f"YAML config file, default {default_config_path()}",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host", help="Bind host, default from PROXY_HOST or 127.0.0.1"
|
"--host", help="Bind host, default from config, PROXY_HOST, or 127.0.0.1"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port", type=int, help="Bind port, default from PROXY_PORT or 9000"
|
"--port",
|
||||||
|
type=int,
|
||||||
|
help="Bind port, default from config, PROXY_PORT, or 9000",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--model", help="Upstream DeepSeek model, default from DEEPSEEK_MODEL"
|
"--model",
|
||||||
|
help="Upstream DeepSeek model, default from config, DEEPSEEK_MODEL, or deepseek-v4-pro",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--base-url", help="DeepSeek base URL, default https://api.deepseek.com"
|
"--base-url",
|
||||||
|
help="DeepSeek base URL, default from config, DEEPSEEK_BASE_URL, or https://api.deepseek.com",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--reasoning-content-path",
|
"--reasoning-content-path",
|
||||||
|
|
@ -505,7 +501,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
|
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
|
||||||
)
|
)
|
||||||
args = build_arg_parser().parse_args(argv)
|
args = build_arg_parser().parse_args(argv)
|
||||||
config = ProxyConfig.from_env(env_file_path=args.config_path)
|
try:
|
||||||
|
config = ProxyConfig.from_file(config_path=args.config_path)
|
||||||
|
except ValueError as exc:
|
||||||
|
LOG.error("%s", exc)
|
||||||
|
return 2
|
||||||
updates: dict[str, Any] = {}
|
updates: dict[str, Any] = {}
|
||||||
if args.host:
|
if args.host:
|
||||||
updates["host"] = args.host
|
updates["host"] = args.host
|
||||||
|
|
@ -528,12 +528,6 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
if updates:
|
if updates:
|
||||||
config = replace(config, **updates)
|
config = replace(config, **updates)
|
||||||
|
|
||||||
try:
|
|
||||||
config.validate()
|
|
||||||
except ValueError as exc:
|
|
||||||
LOG.error("%s", exc)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
store = ReasoningStore(config.reasoning_content_path)
|
store = ReasoningStore(config.reasoning_content_path)
|
||||||
server = DeepSeekProxyServer((config.host, config.port), DeepSeekProxyHandler)
|
server = DeepSeekProxyServer((config.host, config.port), DeepSeekProxyHandler)
|
||||||
server.config = config
|
server.config = config
|
||||||
|
|
@ -572,8 +566,6 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
return 2
|
return 2
|
||||||
LOG.info("ngrok tunnel forwarding %s -> %s", public_url, target_url)
|
LOG.info("ngrok tunnel forwarding %s -> %s", public_url, target_url)
|
||||||
LOG.info("Cursor Base URL: %s/v1", public_url.rstrip("/"))
|
LOG.info("Cursor Base URL: %s/v1", public_url.rstrip("/"))
|
||||||
if config.proxy_api_key:
|
|
||||||
LOG.info("Cursor API key: value of PROXY_API_KEY")
|
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import stat
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
@ -14,12 +15,12 @@ from deepseek_cursor_proxy.config import (
|
||||||
|
|
||||||
|
|
||||||
class ConfigTests(unittest.TestCase):
|
class ConfigTests(unittest.TestCase):
|
||||||
def test_default_paths_live_in_user_app_directory(self) -> None:
|
def test_default_paths_live_in_visible_user_app_directory(self) -> None:
|
||||||
home = Path("/tmp/home")
|
home = Path("/tmp/home")
|
||||||
|
|
||||||
with patch("deepseek_cursor_proxy.config.Path.home", return_value=home):
|
with patch("deepseek_cursor_proxy.config.Path.home", return_value=home):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
default_config_path(), home / ".deepseek-cursor-proxy" / ".env"
|
default_config_path(), home / ".deepseek-cursor-proxy" / "config.yaml"
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
default_reasoning_content_path(),
|
default_reasoning_content_path(),
|
||||||
|
|
@ -30,63 +31,103 @@ class ConfigTests(unittest.TestCase):
|
||||||
home / ".deepseek-cursor-proxy" / "reasoning_content.sqlite3",
|
home / ".deepseek-cursor-proxy" / "reasoning_content.sqlite3",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_loads_config_from_user_env_file(self) -> None:
|
def test_missing_default_config_file_is_populated(self) -> None:
|
||||||
with TemporaryDirectory() as temp_dir:
|
with TemporaryDirectory() as temp_dir:
|
||||||
env_file_path = Path(temp_dir) / ".env"
|
home = Path(temp_dir)
|
||||||
|
|
||||||
|
with patch("deepseek_cursor_proxy.config.Path.home", return_value=home):
|
||||||
|
config = ProxyConfig.from_file(env={}, config_path=None)
|
||||||
|
config_path = default_config_path()
|
||||||
|
|
||||||
|
self.assertTrue(config_path.exists())
|
||||||
|
self.assertIn(
|
||||||
|
"model: deepseek-v4-pro", config_path.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
self.assertEqual(stat.S_IMODE(config_path.stat().st_mode), 0o600)
|
||||||
|
self.assertEqual(config.upstream_model, "deepseek-v4-pro")
|
||||||
|
|
||||||
|
def test_missing_explicit_config_file_is_not_populated(self) -> None:
|
||||||
|
with TemporaryDirectory() as temp_dir:
|
||||||
|
config_path = Path(temp_dir) / "missing.yaml"
|
||||||
|
|
||||||
|
config = ProxyConfig.from_file(env={}, config_path=config_path)
|
||||||
|
|
||||||
|
self.assertFalse(config_path.exists())
|
||||||
|
self.assertEqual(config.upstream_model, "deepseek-v4-pro")
|
||||||
|
|
||||||
|
def test_loads_config_from_user_yaml_file(self) -> None:
|
||||||
|
with TemporaryDirectory() as temp_dir:
|
||||||
|
config_path = Path(temp_dir) / "config.yaml"
|
||||||
reasoning_content_path = Path(temp_dir) / "reasoning_content.sqlite3"
|
reasoning_content_path = Path(temp_dir) / "reasoning_content.sqlite3"
|
||||||
env_file_path.write_text(
|
config_path.write_text(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"DEEPSEEK_API_KEY=file-key",
|
"model: deepseek-v4-flash",
|
||||||
"PROXY_API_KEY=cursor-token",
|
"port: 9100",
|
||||||
"PROXY_PORT=9100",
|
f"reasoning_content_path: {reasoning_content_path}",
|
||||||
f"REASONING_CONTENT_PATH={reasoning_content_path}",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
config = ProxyConfig.from_env(env={}, env_file_path=env_file_path)
|
config = ProxyConfig.from_file(env={}, config_path=config_path)
|
||||||
|
|
||||||
self.assertEqual(config.upstream_api_key, "file-key")
|
self.assertEqual(config.upstream_model, "deepseek-v4-flash")
|
||||||
self.assertEqual(config.proxy_api_key, "cursor-token")
|
|
||||||
self.assertEqual(config.port, 9100)
|
self.assertEqual(config.port, 9100)
|
||||||
self.assertEqual(config.reasoning_content_path, reasoning_content_path)
|
self.assertEqual(config.reasoning_content_path, reasoning_content_path)
|
||||||
|
|
||||||
def test_environment_overrides_config_file(self) -> None:
|
def test_environment_overrides_config_file(self) -> None:
|
||||||
with TemporaryDirectory() as temp_dir:
|
with TemporaryDirectory() as temp_dir:
|
||||||
env_file_path = Path(temp_dir) / ".env"
|
config_path = Path(temp_dir) / "config.yaml"
|
||||||
env_file_path.write_text(
|
config_path.write_text(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"DEEPSEEK_API_KEY=file-key",
|
"verbose: false",
|
||||||
"PROXY_VERBOSE=false",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
config = ProxyConfig.from_env(
|
config = ProxyConfig.from_file(
|
||||||
env={
|
env={
|
||||||
"DEEPSEEK_API_KEY": "env-key",
|
|
||||||
"PROXY_VERBOSE": "true",
|
"PROXY_VERBOSE": "true",
|
||||||
},
|
},
|
||||||
env_file_path=env_file_path,
|
config_path=config_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(config.upstream_api_key, "env-key")
|
|
||||||
self.assertTrue(config.verbose)
|
self.assertTrue(config.verbose)
|
||||||
|
|
||||||
def test_relative_reasoning_content_path_stays_inside_app_directory(self) -> None:
|
def test_relative_reasoning_content_path_in_config_is_relative_to_config_file(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
with TemporaryDirectory() as temp_dir:
|
||||||
|
config_path = Path(temp_dir) / "config.yaml"
|
||||||
|
config_path.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"reasoning_content_path: custom.sqlite3",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = ProxyConfig.from_file(env={}, config_path=config_path)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
config.reasoning_content_path, Path(temp_dir) / "custom.sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_relative_reasoning_content_path_from_env_stays_inside_app_directory(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
home = Path("/tmp/home")
|
home = Path("/tmp/home")
|
||||||
|
|
||||||
with patch("deepseek_cursor_proxy.config.Path.home", return_value=home):
|
with patch("deepseek_cursor_proxy.config.Path.home", return_value=home):
|
||||||
config = ProxyConfig.from_env(
|
config = ProxyConfig.from_file(
|
||||||
env={
|
env={
|
||||||
"DEEPSEEK_API_KEY": "key",
|
|
||||||
"REASONING_CONTENT_PATH": "custom.sqlite3",
|
"REASONING_CONTENT_PATH": "custom.sqlite3",
|
||||||
},
|
},
|
||||||
env_file_path=Path("/does/not/exist"),
|
config_path=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
@ -95,71 +136,83 @@ class ConfigTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_verbose_and_body_logging_can_be_enabled_from_env(self) -> None:
|
def test_verbose_and_body_logging_can_be_enabled_from_env(self) -> None:
|
||||||
config = ProxyConfig.from_env(
|
config = ProxyConfig.from_file(
|
||||||
env={
|
env={
|
||||||
"DEEPSEEK_API_KEY": "key",
|
|
||||||
"PROXY_VERBOSE": "true",
|
"PROXY_VERBOSE": "true",
|
||||||
"PROXY_LOG_BODIES": "1",
|
"PROXY_LOG_BODIES": "1",
|
||||||
"PROXY_NGROK": "yes",
|
"PROXY_NGROK": "yes",
|
||||||
},
|
},
|
||||||
env_file_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.log_bodies)
|
||||||
self.assertTrue(config.ngrok)
|
self.assertTrue(config.ngrok)
|
||||||
|
|
||||||
def test_cursor_reasoning_display_can_be_disabled_from_env(self) -> None:
|
def test_cursor_reasoning_display_can_be_disabled_from_config(self) -> None:
|
||||||
config = ProxyConfig.from_env(
|
with TemporaryDirectory() as temp_dir:
|
||||||
env={
|
config_path = Path(temp_dir) / "config.yaml"
|
||||||
"DEEPSEEK_API_KEY": "key",
|
config_path.write_text(
|
||||||
"CURSOR_DISPLAY_REASONING": "false",
|
"\n".join(
|
||||||
},
|
[
|
||||||
env_file_path=Path("/does/not/exist"),
|
"display_reasoning: false",
|
||||||
)
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = ProxyConfig.from_file(env={}, config_path=config_path)
|
||||||
|
|
||||||
self.assertFalse(config.cursor_display_reasoning)
|
self.assertFalse(config.cursor_display_reasoning)
|
||||||
|
|
||||||
def test_config_path_can_be_overridden_from_environment(self) -> None:
|
def test_config_path_can_be_overridden_from_environment(self) -> None:
|
||||||
with TemporaryDirectory() as temp_dir:
|
with TemporaryDirectory() as temp_dir:
|
||||||
first_env_path = Path(temp_dir) / "first.env"
|
first_config_path = Path(temp_dir) / "first.yaml"
|
||||||
second_env_path = Path(temp_dir) / "second.env"
|
second_config_path = Path(temp_dir) / "second.yaml"
|
||||||
first_env_path.write_text("DEEPSEEK_API_KEY=first-key", encoding="utf-8")
|
first_config_path.write_text("port: 9100\n", encoding="utf-8")
|
||||||
second_env_path.write_text("DEEPSEEK_API_KEY=second-key", encoding="utf-8")
|
second_config_path.write_text("port: 9200\n", encoding="utf-8")
|
||||||
|
|
||||||
config = ProxyConfig.from_env(
|
config = ProxyConfig.from_file(
|
||||||
env={"DEEPSEEK_CURSOR_PROXY_CONFIG_PATH": str(second_env_path)},
|
env={"DEEPSEEK_CURSOR_PROXY_CONFIG_PATH": str(second_config_path)},
|
||||||
env_file_path=None,
|
config_path=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(config.upstream_api_key, "second-key")
|
self.assertEqual(config.port, 9200)
|
||||||
|
|
||||||
def test_explicit_env_file_path_wins_over_config_path_environment_variable(
|
def test_explicit_config_file_path_wins_over_config_path_environment_variable(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
with TemporaryDirectory() as temp_dir:
|
with TemporaryDirectory() as temp_dir:
|
||||||
first_env_path = Path(temp_dir) / "first.env"
|
first_config_path = Path(temp_dir) / "first.yaml"
|
||||||
second_env_path = Path(temp_dir) / "second.env"
|
second_config_path = Path(temp_dir) / "second.yaml"
|
||||||
first_env_path.write_text("DEEPSEEK_API_KEY=first-key", encoding="utf-8")
|
first_config_path.write_text("port: 9100\n", encoding="utf-8")
|
||||||
second_env_path.write_text("DEEPSEEK_API_KEY=second-key", encoding="utf-8")
|
second_config_path.write_text("port: 9200\n", encoding="utf-8")
|
||||||
|
|
||||||
config = ProxyConfig.from_env(
|
config = ProxyConfig.from_file(
|
||||||
env={"DEEPSEEK_CURSOR_PROXY_CONFIG_PATH": str(second_env_path)},
|
env={"DEEPSEEK_CURSOR_PROXY_CONFIG_PATH": str(second_config_path)},
|
||||||
env_file_path=first_env_path,
|
config_path=first_config_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(config.upstream_api_key, "first-key")
|
self.assertEqual(config.port, 9100)
|
||||||
|
|
||||||
def test_from_env_does_not_mutate_process_environment(self) -> None:
|
def test_invalid_yaml_config_raises_value_error(self) -> None:
|
||||||
|
with TemporaryDirectory() as temp_dir:
|
||||||
|
config_path = Path(temp_dir) / "config.yaml"
|
||||||
|
config_path.write_text("- not\n- a\n- mapping\n", encoding="utf-8")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
ProxyConfig.from_file(env={}, config_path=config_path)
|
||||||
|
|
||||||
|
def test_from_file_does_not_mutate_process_environment(self) -> None:
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
"os.environ",
|
"os.environ",
|
||||||
{
|
{
|
||||||
"DEEPSEEK_API_KEY": "env-key",
|
"PROXY_VERBOSE": "true",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
ProxyConfig.from_env(env_file_path=Path("/does/not/exist"))
|
ProxyConfig.from_file(config_path=Path("/does/not/exist"))
|
||||||
self.assertEqual(dict(os.environ), {"DEEPSEEK_API_KEY": "env-key"})
|
self.assertEqual(dict(os.environ), {"PROXY_VERBOSE": "true"})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ from deepseek_cursor_proxy.reasoning_store import ReasoningStore
|
||||||
from deepseek_cursor_proxy.server import DeepSeekProxyHandler, DeepSeekProxyServer
|
from deepseek_cursor_proxy.server import DeepSeekProxyHandler, DeepSeekProxyServer
|
||||||
|
|
||||||
|
|
||||||
LIVE_DEEPSEEK = os.getenv("RUN_LIVE_DEEPSEEK_TESTS") == "1"
|
LIVE_DEEPSEEK = os.getenv("RUN_LIVE_DEEPSEEK_TESTS") == "1" and bool(
|
||||||
|
os.getenv("LIVE_DEEPSEEK_KEY")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_json(
|
def post_json(
|
||||||
|
|
@ -37,12 +39,10 @@ def post_json(
|
||||||
|
|
||||||
|
|
||||||
class ProxyFixture:
|
class ProxyFixture:
|
||||||
def __init__(self, api_key: str) -> None:
|
def __init__(self) -> None:
|
||||||
self.store = ReasoningStore(":memory:")
|
self.store = ReasoningStore(":memory:")
|
||||||
server = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
server = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
||||||
server.config = ProxyConfig(
|
server.config = ProxyConfig(
|
||||||
upstream_api_key=api_key,
|
|
||||||
proxy_api_key="cursor-local-token",
|
|
||||||
upstream_base_url="https://api.deepseek.com",
|
upstream_base_url="https://api.deepseek.com",
|
||||||
upstream_model="deepseek-v4-pro",
|
upstream_model="deepseek-v4-pro",
|
||||||
request_timeout=180,
|
request_timeout=180,
|
||||||
|
|
@ -68,17 +68,18 @@ class ProxyFixture:
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
LIVE_DEEPSEEK, "set RUN_LIVE_DEEPSEEK_TESTS=1 to run live DeepSeek API tests"
|
LIVE_DEEPSEEK,
|
||||||
|
"set RUN_LIVE_DEEPSEEK_TESTS=1 and LIVE_DEEPSEEK_KEY to run live tests",
|
||||||
)
|
)
|
||||||
class LiveDeepSeekProxyTests(unittest.TestCase):
|
class LiveDeepSeekProxyTests(unittest.TestCase):
|
||||||
def test_proxy_repairs_real_deepseek_tool_call_history(self) -> None:
|
def test_proxy_repairs_real_deepseek_tool_call_history(self) -> None:
|
||||||
api_key = os.environ["DEEPSEEK_API_KEY"]
|
api_key = os.environ["LIVE_DEEPSEEK_KEY"]
|
||||||
proxy = ProxyFixture(api_key).start()
|
proxy = ProxyFixture().start()
|
||||||
try:
|
try:
|
||||||
first_status, first_response = post_json(
|
first_status, first_response = post_json(
|
||||||
proxy.url,
|
proxy.url,
|
||||||
first_request(),
|
first_request(),
|
||||||
api_key="cursor-local-token",
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
self.assertEqual(first_status, 200, first_response.get("error"))
|
self.assertEqual(first_status, 200, first_response.get("error"))
|
||||||
assistant_with_reasoning = first_response["choices"][0]["message"]
|
assistant_with_reasoning = first_response["choices"][0]["message"]
|
||||||
|
|
@ -118,7 +119,7 @@ class LiveDeepSeekProxyTests(unittest.TestCase):
|
||||||
proxy_status, second_response = post_json(
|
proxy_status, second_response = post_json(
|
||||||
proxy.url,
|
proxy.url,
|
||||||
missing_reasoning_payload,
|
missing_reasoning_payload,
|
||||||
api_key="cursor-local-token",
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
self.assertEqual(proxy_status, 200, second_response.get("error"))
|
self.assertEqual(proxy_status, 200, second_response.get("error"))
|
||||||
final_assistant = second_response["choices"][0]["message"]
|
final_assistant = second_response["choices"][0]["message"]
|
||||||
|
|
@ -145,7 +146,7 @@ class LiveDeepSeekProxyTests(unittest.TestCase):
|
||||||
followup_status, followup_response = post_json(
|
followup_status, followup_response = post_json(
|
||||||
proxy.url,
|
proxy.url,
|
||||||
followup_payload,
|
followup_payload,
|
||||||
api_key="cursor-local-token",
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
self.assertEqual(followup_status, 200, followup_response.get("error"))
|
self.assertEqual(followup_status, 200, followup_response.get("error"))
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ FINAL_CONTENT = "Final answer after using the tool."
|
||||||
|
|
||||||
|
|
||||||
def post_json(
|
def post_json(
|
||||||
url: str, payload: dict, api_key: str = "cursor-local-token"
|
url: str, payload: dict, api_key: str = "sk-cursor-test"
|
||||||
) -> tuple[int, dict]:
|
) -> tuple[int, dict]:
|
||||||
body = json.dumps(payload).encode("utf-8")
|
body = json.dumps(payload).encode("utf-8")
|
||||||
request = Request(
|
request = Request(
|
||||||
|
|
@ -45,6 +45,7 @@ def post_json(
|
||||||
|
|
||||||
class FakeDeepSeekHandler(BaseHTTPRequestHandler):
|
class FakeDeepSeekHandler(BaseHTTPRequestHandler):
|
||||||
requests: list[dict] = []
|
requests: list[dict] = []
|
||||||
|
auth_headers: list[str] = []
|
||||||
|
|
||||||
def log_message(self, fmt: str, *args: object) -> None:
|
def log_message(self, fmt: str, *args: object) -> None:
|
||||||
return
|
return
|
||||||
|
|
@ -53,6 +54,7 @@ class FakeDeepSeekHandler(BaseHTTPRequestHandler):
|
||||||
length = int(self.headers.get("Content-Length") or 0)
|
length = int(self.headers.get("Content-Length") or 0)
|
||||||
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||||
self.__class__.requests.append(payload)
|
self.__class__.requests.append(payload)
|
||||||
|
self.__class__.auth_headers.append(self.headers.get("Authorization", ""))
|
||||||
|
|
||||||
for index, message in enumerate(payload.get("messages", [])):
|
for index, message in enumerate(payload.get("messages", [])):
|
||||||
if not isinstance(message, dict) or message.get("role") != "assistant":
|
if not isinstance(message, dict) or message.get("role") != "assistant":
|
||||||
|
|
@ -383,14 +385,13 @@ class ServerFixture:
|
||||||
class ProxyEndToEndTests(unittest.TestCase):
|
class ProxyEndToEndTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
FakeDeepSeekHandler.requests = []
|
FakeDeepSeekHandler.requests = []
|
||||||
|
FakeDeepSeekHandler.auth_headers = []
|
||||||
self.upstream = ServerFixture(
|
self.upstream = ServerFixture(
|
||||||
ThreadingHTTPServer(("127.0.0.1", 0), FakeDeepSeekHandler)
|
ThreadingHTTPServer(("127.0.0.1", 0), FakeDeepSeekHandler)
|
||||||
).start()
|
).start()
|
||||||
self.store = ReasoningStore(":memory:")
|
self.store = ReasoningStore(":memory:")
|
||||||
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
||||||
proxy.config = ProxyConfig(
|
proxy.config = ProxyConfig(
|
||||||
upstream_api_key="upstream-key",
|
|
||||||
proxy_api_key="cursor-local-token",
|
|
||||||
upstream_base_url=self.upstream.url,
|
upstream_base_url=self.upstream.url,
|
||||||
upstream_model="deepseek-v4-pro",
|
upstream_model="deepseek-v4-pro",
|
||||||
)
|
)
|
||||||
|
|
@ -451,6 +452,30 @@ class ProxyEndToEndTests(unittest.TestCase):
|
||||||
third_upstream_messages[3]["reasoning_content"], FINAL_REASONING
|
third_upstream_messages[3]["reasoning_content"], FINAL_REASONING
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_proxy_forwards_cursor_bearer_token_to_deepseek(self) -> None:
|
||||||
|
status, _ = post_json(
|
||||||
|
f"{self.proxy.url}/v1/chat/completions",
|
||||||
|
first_cursor_request(),
|
||||||
|
api_key="sk-from-cursor",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(FakeDeepSeekHandler.auth_headers[0], "Bearer sk-from-cursor")
|
||||||
|
|
||||||
|
def test_proxy_rejects_missing_cursor_bearer_token(self) -> None:
|
||||||
|
request = Request(
|
||||||
|
f"{self.proxy.url}/v1/chat/completions",
|
||||||
|
data=json.dumps(first_cursor_request()).encode("utf-8"),
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(HTTPError) as caught:
|
||||||
|
urlopen(request, timeout=5)
|
||||||
|
|
||||||
|
self.assertEqual(caught.exception.code, 401)
|
||||||
|
self.assertEqual(FakeDeepSeekHandler.requests, [])
|
||||||
|
|
||||||
def test_proxy_adds_fallback_reasoning_for_uncached_cursor_tool_history(
|
def test_proxy_adds_fallback_reasoning_for_uncached_cursor_tool_history(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -473,8 +498,6 @@ class InterleavedConversationTests(unittest.TestCase):
|
||||||
self.store = ReasoningStore(":memory:")
|
self.store = ReasoningStore(":memory:")
|
||||||
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
||||||
proxy.config = ProxyConfig(
|
proxy.config = ProxyConfig(
|
||||||
upstream_api_key="upstream-key",
|
|
||||||
proxy_api_key="cursor-local-token",
|
|
||||||
upstream_base_url=self.upstream.url,
|
upstream_base_url=self.upstream.url,
|
||||||
upstream_model="deepseek-v4-pro",
|
upstream_model="deepseek-v4-pro",
|
||||||
)
|
)
|
||||||
|
|
@ -580,8 +603,6 @@ class StreamingProxyTests(unittest.TestCase):
|
||||||
self.store = ReasoningStore(":memory:")
|
self.store = ReasoningStore(":memory:")
|
||||||
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
||||||
proxy.config = ProxyConfig(
|
proxy.config = ProxyConfig(
|
||||||
upstream_api_key="upstream-key",
|
|
||||||
proxy_api_key="cursor-local-token",
|
|
||||||
upstream_base_url=self.upstream.url,
|
upstream_base_url=self.upstream.url,
|
||||||
upstream_model="deepseek-v4-pro",
|
upstream_model="deepseek-v4-pro",
|
||||||
)
|
)
|
||||||
|
|
@ -607,7 +628,7 @@ class StreamingProxyTests(unittest.TestCase):
|
||||||
).encode("utf-8"),
|
).encode("utf-8"),
|
||||||
method="POST",
|
method="POST",
|
||||||
headers={
|
headers={
|
||||||
"Authorization": "Bearer cursor-local-token",
|
"Authorization": "Bearer sk-cursor-test",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -629,8 +650,6 @@ class ReasoningStreamingProxyTests(unittest.TestCase):
|
||||||
self.store = ReasoningStore(":memory:")
|
self.store = ReasoningStore(":memory:")
|
||||||
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
proxy = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
||||||
proxy.config = ProxyConfig(
|
proxy.config = ProxyConfig(
|
||||||
upstream_api_key="upstream-key",
|
|
||||||
proxy_api_key="cursor-local-token",
|
|
||||||
upstream_base_url=self.upstream.url,
|
upstream_base_url=self.upstream.url,
|
||||||
upstream_model="deepseek-v4-pro",
|
upstream_model="deepseek-v4-pro",
|
||||||
)
|
)
|
||||||
|
|
@ -657,7 +676,7 @@ class ReasoningStreamingProxyTests(unittest.TestCase):
|
||||||
).encode("utf-8"),
|
).encode("utf-8"),
|
||||||
method="POST",
|
method="POST",
|
||||||
headers={
|
headers={
|
||||||
"Authorization": "Bearer cursor-local-token",
|
"Authorization": "Bearer sk-cursor-test",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.payload["messages"][1]["content"], "Visible answer.")
|
self.assertEqual(prepared.payload["messages"][1]["content"], "Visible answer.")
|
||||||
|
|
||||||
|
|
@ -72,7 +70,7 @@ class TransformTests(unittest.TestCase):
|
||||||
"max_completion_tokens": 123,
|
"max_completion_tokens": 123,
|
||||||
"parallel_tool_calls": True,
|
"parallel_tool_calls": True,
|
||||||
}
|
}
|
||||||
config = ProxyConfig(upstream_api_key="key")
|
config = ProxyConfig()
|
||||||
|
|
||||||
prepared = prepare_upstream_request(payload, config, self.store)
|
prepared = prepare_upstream_request(payload, config, self.store)
|
||||||
|
|
||||||
|
|
@ -97,9 +95,7 @@ class TransformTests(unittest.TestCase):
|
||||||
"tool_choice": "required",
|
"tool_choice": "required",
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.payload["tool_choice"], "auto")
|
self.assertEqual(prepared.payload["tool_choice"], "auto")
|
||||||
|
|
||||||
|
|
@ -147,9 +143,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.patched_reasoning_messages, 1)
|
self.assertEqual(prepared.patched_reasoning_messages, 1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
@ -212,9 +206,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.patched_reasoning_messages, 1)
|
self.assertEqual(prepared.patched_reasoning_messages, 1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
@ -261,12 +253,8 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared_a = prepare_upstream_request(
|
prepared_a = prepare_upstream_request(payload_a, ProxyConfig(), self.store)
|
||||||
payload_a, ProxyConfig(upstream_api_key="key"), self.store
|
prepared_b = prepare_upstream_request(payload_b, ProxyConfig(), self.store)
|
||||||
)
|
|
||||||
prepared_b = prepare_upstream_request(
|
|
||||||
payload_b, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
prepared_a.payload["messages"][1]["reasoning_content"],
|
prepared_a.payload["messages"][1]["reasoning_content"],
|
||||||
|
|
@ -321,7 +309,7 @@ class TransformTests(unittest.TestCase):
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ProxyConfig(upstream_api_key="key"),
|
ProxyConfig(),
|
||||||
self.store,
|
self.store,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -370,9 +358,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.patched_reasoning_messages, 1)
|
self.assertEqual(prepared.patched_reasoning_messages, 1)
|
||||||
self.assertEqual(prepared.payload["messages"][1]["content"], "")
|
self.assertEqual(prepared.payload["messages"][1]["content"], "")
|
||||||
|
|
@ -415,7 +401,7 @@ class TransformTests(unittest.TestCase):
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ProxyConfig(upstream_api_key="key"),
|
ProxyConfig(),
|
||||||
self.store,
|
self.store,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -453,9 +439,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.patched_reasoning_messages, 0)
|
self.assertEqual(prepared.patched_reasoning_messages, 0)
|
||||||
self.assertEqual(prepared.fallback_reasoning_messages, 1)
|
self.assertEqual(prepared.fallback_reasoning_messages, 1)
|
||||||
|
|
@ -493,9 +477,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.fallback_reasoning_messages, 1)
|
self.assertEqual(prepared.fallback_reasoning_messages, 1)
|
||||||
self.assertIn("reasoning_content", prepared.payload["messages"][3])
|
self.assertIn("reasoning_content", prepared.payload["messages"][3])
|
||||||
|
|
@ -510,9 +492,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(prepared.fallback_reasoning_messages, 0)
|
self.assertEqual(prepared.fallback_reasoning_messages, 0)
|
||||||
self.assertNotIn("reasoning_content", prepared.payload["messages"][1])
|
self.assertNotIn("reasoning_content", prepared.payload["messages"][1])
|
||||||
|
|
@ -530,9 +510,7 @@ class TransformTests(unittest.TestCase):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared = prepare_upstream_request(
|
prepared = prepare_upstream_request(payload, ProxyConfig(), self.store)
|
||||||
payload, ProxyConfig(upstream_api_key="key"), self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
prepared.payload["messages"][0],
|
prepared.payload["messages"][0],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue