-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathapi.py
More file actions
144 lines (113 loc) · 4.71 KB
/
Copy pathapi.py
File metadata and controls
144 lines (113 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""
OpenAI-compatible FastAPI server for DeepSeek.
Point any OpenAI client at http://localhost:8000/v1 :
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
r = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Hello!"}],
)
Endpoints:
GET /v1/models
POST /v1/chat/completions (stream=true supported)
GET /healthz
Requests under /v1 are rate limited per client IP (default 30/min, set via
RATE_LIMIT_PER_MINUTE); /healthz is exempt.
"""
from __future__ import annotations
import threading
import time
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.concurrency import run_in_threadpool
from deepseek.auth import LoginRequired
from deepseek.client import DeepSeekClient
from .config import (
MODEL_MAP,
RATE_LIMIT_PER_MINUTE,
SERVER_INTERACTIVE_LOGIN,
is_known_model,
resolve_model_type,
)
from .openai_format import completion_response, messages_to_prompt, stream_chunks
from .ratelimit import RateLimiter, install_rate_limit
from .schemas import ChatCompletionRequest
load_dotenv()
app = FastAPI(title="DeepSeek OpenAI-compatible API", version="0.1.0")
install_rate_limit(app, RateLimiter(limit=RATE_LIMIT_PER_MINUTE, window=60.0))
# One shared client (and its signed-in session) built lazily on first use.
_client: DeepSeekClient | None = None
_client_lock = threading.Lock()
def get_client() -> DeepSeekClient:
"""Build (once) the shared client and its signed-in session.
Session resolution: cached file → headless capture off the persistent
profile. If neither works and SERVER_INTERACTIVE_LOGIN is on (the default),
it opens a visible browser window so you can sign in — the triggering
request blocks until you finish. If interactive login is off, it raises
`LoginRequired`, which the endpoint turns into an actionable 503.
This touches Playwright's sync API, so callers must invoke it OFF the event
loop (via run_in_threadpool); calling it inside the asyncio loop raises
"Playwright Sync API inside the asyncio loop"."""
global _client
if _client is None:
with _client_lock:
if _client is None:
_client = DeepSeekClient(allow_interactive=SERVER_INTERACTIVE_LOGIN)
return _client
def _error(message: str, status: int = 500, err_type: str = "server_error"):
return JSONResponse(
status_code=status,
content={"error": {"message": message, "type": err_type}},
)
@app.get("/healthz")
def healthz():
return {"status": "ok"}
@app.get("/v1/models")
def list_models():
created = int(time.time())
return {
"object": "list",
"data": [
{"id": name, "object": "model", "created": created, "owned_by": "deepseek"}
for name in MODEL_MAP
],
}
@app.post("/v1/chat/completions")
async def chat_completions(req: ChatCompletionRequest):
if not req.messages:
return _error("`messages` must not be empty", status=400, err_type="invalid_request_error")
if not is_known_model(req.model):
return _error(
f"The model `{req.model}` does not exist. Available models: "
f"{', '.join(MODEL_MAP)}",
status=404, err_type="model_not_found",
)
# A thread's model is fixed when it's created, so on resume we ignore `model`
# (the OpenAI SDK always sends one) and let the existing thread's model stand.
model_type = None if req.conversation_id else resolve_model_type(req.model)
prompt = messages_to_prompt(req.messages)
try:
# Off the event loop: get_client() uses Playwright's sync API, which
# errors if run inside the asyncio loop.
client = await run_in_threadpool(get_client)
except LoginRequired as e:
return _error(str(e), status=503, err_type="login_required")
except Exception as e: # session/login failure
return _error(f"Failed to initialise DeepSeek session: {e}")
if req.stream:
def gen():
stream = client.stream(
prompt, conversation_id=req.conversation_id,
model=model_type, thinking=req.thinking, search=req.search,
)
yield from stream_chunks(req.model, stream)
return StreamingResponse(gen(), media_type="text/event-stream")
try:
reply = await run_in_threadpool(
client.chat, prompt, req.conversation_id,
model_type, req.thinking, req.search,
)
except Exception as e:
return _error(f"DeepSeek request failed: {e}")
return completion_response(req.model, reply.text, prompt, reply.conversation_id)