181 lines
6.1 KiB
Python
181 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
import json
|
|
import os
|
|
import threading
|
|
import unittest
|
|
from urllib.error import HTTPError
|
|
from urllib.request import Request, urlopen
|
|
|
|
from deepseek_cursor_proxy.config import ProxyConfig
|
|
from deepseek_cursor_proxy.reasoning_store import ReasoningStore
|
|
from deepseek_cursor_proxy.server import DeepSeekProxyHandler, DeepSeekProxyServer
|
|
|
|
|
|
LIVE_DEEPSEEK = os.getenv("RUN_LIVE_DEEPSEEK_TESTS") == "1" and bool(
|
|
os.getenv("LIVE_DEEPSEEK_KEY")
|
|
)
|
|
|
|
|
|
def post_json(
|
|
url: str, payload: dict, api_key: str, timeout: int = 180
|
|
) -> tuple[int, dict]:
|
|
request = Request(
|
|
url,
|
|
data=json.dumps(payload).encode("utf-8"),
|
|
method="POST",
|
|
headers={
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
try:
|
|
response = urlopen(request, timeout=timeout)
|
|
with response:
|
|
return response.status, json.loads(response.read().decode("utf-8"))
|
|
except HTTPError as exc:
|
|
return exc.code, json.loads(exc.read().decode("utf-8"))
|
|
|
|
|
|
class ProxyFixture:
|
|
def __init__(self) -> None:
|
|
self.store = ReasoningStore(":memory:")
|
|
server = DeepSeekProxyServer(("127.0.0.1", 0), DeepSeekProxyHandler)
|
|
server.config = ProxyConfig(
|
|
upstream_base_url="https://api.deepseek.com",
|
|
upstream_model="deepseek-v4-pro",
|
|
request_timeout=180,
|
|
)
|
|
server.reasoning_store = self.store
|
|
self.server = server
|
|
self.thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
|
|
@property
|
|
def url(self) -> str:
|
|
host, port = self.server.server_address
|
|
return f"http://{host}:{port}/v1/chat/completions"
|
|
|
|
def start(self) -> "ProxyFixture":
|
|
self.thread.start()
|
|
return self
|
|
|
|
def close(self) -> None:
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
self.thread.join(timeout=5)
|
|
self.store.close()
|
|
|
|
|
|
@unittest.skipUnless(
|
|
LIVE_DEEPSEEK,
|
|
"set RUN_LIVE_DEEPSEEK_TESTS=1 and LIVE_DEEPSEEK_KEY to run live tests",
|
|
)
|
|
class LiveDeepSeekProxyTests(unittest.TestCase):
|
|
def test_proxy_repairs_real_deepseek_tool_call_history(self) -> None:
|
|
api_key = os.environ["LIVE_DEEPSEEK_KEY"]
|
|
proxy = ProxyFixture().start()
|
|
try:
|
|
first_status, first_response = post_json(
|
|
proxy.url,
|
|
first_request(),
|
|
api_key=api_key,
|
|
)
|
|
self.assertEqual(first_status, 200, first_response.get("error"))
|
|
assistant_with_reasoning = first_response["choices"][0]["message"]
|
|
self.assertTrue(assistant_with_reasoning.get("reasoning_content"))
|
|
self.assertTrue(assistant_with_reasoning.get("tool_calls"))
|
|
|
|
cursor_assistant = deepcopy(assistant_with_reasoning)
|
|
cursor_assistant.pop("reasoning_content", None)
|
|
tool_messages = [
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": tool_call["id"],
|
|
"content": "2026-04-24",
|
|
}
|
|
for tool_call in cursor_assistant["tool_calls"]
|
|
]
|
|
missing_reasoning_payload = {
|
|
"model": "deepseek-v4-pro",
|
|
"messages": [
|
|
first_request()["messages"][0],
|
|
cursor_assistant,
|
|
*tool_messages,
|
|
],
|
|
"tools": first_request()["tools"],
|
|
"thinking": {"type": "enabled"},
|
|
"reasoning_effort": "high",
|
|
}
|
|
|
|
direct_status, direct_response = post_json(
|
|
"https://api.deepseek.com/chat/completions",
|
|
missing_reasoning_payload,
|
|
api_key=api_key,
|
|
)
|
|
self.assertEqual(direct_status, 400)
|
|
self.assertIn("reasoning_content", direct_response["error"]["message"])
|
|
|
|
proxy_status, second_response = post_json(
|
|
proxy.url,
|
|
missing_reasoning_payload,
|
|
api_key=api_key,
|
|
)
|
|
self.assertEqual(proxy_status, 200, second_response.get("error"))
|
|
final_assistant = second_response["choices"][0]["message"]
|
|
self.assertTrue(
|
|
final_assistant.get("content") or final_assistant.get("tool_calls")
|
|
)
|
|
|
|
if final_assistant.get("content"):
|
|
cursor_final = deepcopy(final_assistant)
|
|
cursor_final.pop("reasoning_content", None)
|
|
followup_payload = {
|
|
"model": "deepseek-v4-pro",
|
|
"messages": [
|
|
first_request()["messages"][0],
|
|
cursor_assistant,
|
|
*tool_messages,
|
|
cursor_final,
|
|
{"role": "user", "content": "Reply with exactly: OK"},
|
|
],
|
|
"tools": first_request()["tools"],
|
|
"thinking": {"type": "enabled"},
|
|
"reasoning_effort": "high",
|
|
}
|
|
followup_status, followup_response = post_json(
|
|
proxy.url,
|
|
followup_payload,
|
|
api_key=api_key,
|
|
)
|
|
self.assertEqual(followup_status, 200, followup_response.get("error"))
|
|
finally:
|
|
proxy.close()
|
|
|
|
|
|
def first_request() -> dict:
|
|
return {
|
|
"model": "deepseek-v4-pro",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "Use the get_date tool exactly once, then tell me the date it returns.",
|
|
}
|
|
],
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_date",
|
|
"description": "Return the current date as YYYY-MM-DD.",
|
|
"parameters": {"type": "object", "properties": {}},
|
|
},
|
|
}
|
|
],
|
|
"tool_choice": "required",
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|