Unofficial Python client for DeepSeek — supports default & expert models, thinking, web search, and file/image uploads.
Project description
p2d-deepseek
Unofficial Python client for DeepSeek. Supports default and expert models with thinking, web search, and file/image uploads — no special API needed, just your DeepSeek auth token.
Installation
Using pip (recommended):
pip install p2d-deepseek
Upgrade to latest version:
pip install p2d-deepseek --upgrade
Install specific version:
pip install p2d-deepseek==0.1.9
Note: The PyPI package is named
p2d-deepseekbut the Python import name isdeepseek. Install withpip install p2d-deepseek, then usefrom deepseek import DeepSeekClientin your code.
Getting Your Auth Token
To use this package, you need your DeepSeek auth token. Choose whichever method suits you best.
Method 1 — LocalStorage (Fastest, Desktop)
- Go to chat.deepseek.com and log in
- Open DevTools — press
F12or right-click anywhere → Inspect - Go to the Application tab (if hidden, click
»to find it) - In the left sidebar, expand Local Storage → click
https://chat.deepseek.com - Find the key called
userToken - Copy the value field — that is your token
Method 2 — Network Tab (Desktop)
- Go to chat.deepseek.com and log in
- Open DevTools → Network tab
- Send any message in the chat
- Click on any request going to
chat.deepseek.com - Open the Headers section
- Find the
authorizationheader and copy its value (without theBearerprefix)
Method 3 — Kiwi Browser (Android / Mobile)
- Install Kiwi Browser from the Play Store
- Open chat.deepseek.com and log in
- Tap the menu (⋮) → Developer Tools
- Go to the Application tab → Local Storage →
https://chat.deepseek.com - Find the key called
userTokenand copy the value field
Note: Kiwi Browser is only available on Android. iPhone users can use a laptop or PC with any of the desktop methods above.
Cloudflare Issues
If you see a "Just a moment..." page or requests are being blocked:
- Try logging out and back in on chat.deepseek.com
- Wait a few minutes, then grab a fresh token
- If the problem keeps happening, switch to a different network (e.g. mobile data vs Wi-Fi)
- Avoid making too many requests in a short time
Your token is tied to your session. If DeepSeek logs you out or your session expires, repeat any of the steps above to get a fresh one.
Quick Start
from deepseek import DeepSeekClient
client = DeepSeekClient(api_key="YOUR_TOKEN_HERE")
response = client.chat("What is the capital of France?")
print(response.response)
Models
| Model | Description |
|---|---|
default |
Standard DeepSeek model — fast, general purpose |
expert |
Expert model — deeper reasoning, more detailed answers |
Feature Flags
| Flag | Type | Default | Description |
|---|---|---|---|
model |
str |
"default" |
Model to use: "default" or "expert" |
thinking |
bool |
False |
Enable chain-of-thought reasoning |
search |
bool |
False |
Enable live web search |
session_id |
str |
None |
Reuse an existing chat session |
parent_message_id |
int |
None |
Override which message in the session this reply continues from. The client tracks this automatically — set it only if you want to branch off an earlier turn. |
files |
list[str] |
None |
Local file paths to upload and attach to this prompt |
file_ids |
list[str] |
None |
Already-uploaded file IDs (from client.upload_file()) to attach |
Response Fields
Every client.chat() call returns a ChatResponse object with these fields:
| Field | Type | Description |
|---|---|---|
response |
str |
Clean final answer from the model |
thinking_content |
str | None |
Raw thinking process (only when thinking=True, else None) |
full_response |
str |
Thinking first + answer below — use this in bots when thinking is enabled |
answer |
str |
Same as response — clean final answer only |
session_id |
str |
Session ID — use to continue conversations |
message_id |
int |
Message ID in the session |
model_type |
str |
Model used (default or expert) |
thinking_enabled |
bool |
Whether thinking was enabled |
search_enabled |
bool |
Whether web search was enabled |
status |
str |
Response status from DeepSeek |
Thinking Mode — Important
When thinking=True, the model shows its internal reasoning before giving the final answer.
Which field to use?
response = client.chat("What is 17 * 23?", thinking=True)
response.response # "391" ← just the clean answer
response.thinking_content # "We need to compute 17 * 23..." ← just the thinking
response.full_response # thinking first + answer below (combined in one string)
In a bot — always use full_response
response = client.chat(user_message, thinking=True)
# WRONG — sends two separate messages, thinking appears twice
await message.reply(response.response)
await message.reply(response.thinking_content)
# CORRECT — one message, thinking first, answer below
await message.reply(response.full_response)
Without thinking — use full_response
full_response works correctly whether thinking is on or off — always use it in bots.
response = client.chat(user_message) # thinking=False by default
# WRONG — response.response works but is inconsistent; breaks if you later enable thinking
await message.reply(response.response)
# CORRECT — full_response returns just the clean answer when thinking=False
await message.reply(response.full_response)
full_response when thinking is disabled
If thinking=False, full_response returns the same thing as response — no thinking content, just the clean answer. So you can safely always use full_response and it will work correctly in both cases.
# Works correctly whether thinking=True or thinking=False
await message.reply(response.full_response)
File & Image Uploads
Attach images, PDFs, code files, spreadsheets, and more to any prompt. The client uploads the file, waits for DeepSeek to parse it, and includes it as a reference for the model.
Quickest way — pass files directly to chat()
response = client.chat(
"What is in this image?",
files=["path/to/photo.jpg"],
)
print(response.response)
You can attach multiple files at once:
response = client.chat(
"Compare these two screenshots",
files=["before.png", "after.png"],
)
Upload once, reuse the same file across multiple chats
If you want to ask several questions about the same file without re-uploading every time, upload it once and reuse the returned file_id:
file_id = client.upload_file("report.pdf")
r1 = client.chat("Summarise this PDF in 3 bullets", file_ids=[file_id])
r2 = client.chat("What conclusion does the author reach?", file_ids=[file_id], session_id=r1.session_id)
r3 = client.chat("List every cited source", file_ids=[file_id], session_id=r1.session_id)
Mix uploaded IDs with new files in the same prompt
existing_id = client.upload_file("contract.pdf")
response = client.chat(
"Are these two pages from the same contract?",
files=["page_scan.jpg"], # uploaded now
file_ids=[existing_id], # already uploaded earlier
)
Inspect uploaded file metadata
infos = client.fetch_files([file_id])
print(infos[0])
# {
# "id": "file-...",
# "file_name": "report.pdf",
# "status": "SUCCESS",
# "file_size": 234567,
# "is_image": False,
# "token_usage": 812,
# ...
# }
Supported file types
DeepSeek accepts a wide range of files. Common ones that work out of the box:
| Category | Extensions |
|---|---|
| Images | .jpg, .jpeg, .png, .gif, .webp, .bmp, .heic, .heif, .tiff |
| Documents | .pdf |
| Office | .doc, .docx, .xls, .xlsx, .ppt, .pptx |
| Text & Markdown | .txt, .md, .rtf |
| Code | .py, .js, .ts, .tsx, .jsx, .java, .kt, .swift, .c, .h, .cpp, .hpp, .cs, .go, .rs, .rb, .php, .sh, .bash, .zsh, .lua, .r, .scala, .dart |
| Web | .html, .htm, .css, .scss, .vue, .svelte |
| Data | .json, .xml, .yaml, .yml, .toml, .csv, .tsv, .sql, .ipynb, .log |
If a file type isn't recognised, the client falls back to
application/octet-stream. DeepSeek will still try to parse it.
upload_file() parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
file_path |
str |
required | Local path to the file |
model |
str |
"default" |
Model that will use the file (just affects upload allocation) |
thinking |
bool |
False |
Whether the upload should be flagged for thinking-mode usage |
poll_interval |
float |
1.0 |
Seconds between status checks while DeepSeek parses the file |
timeout |
float |
120.0 |
Maximum seconds to wait for parsing before erroring out |
Returns: the file_id string (e.g. "file-a7be4dc1-07b8-4911-b98a-02e79266ca7d") once status is SUCCESS.
Continuing a Conversation
Pass the previous session_id back into chat() to keep the context. The client remembers the last message_id per session, so each new call appends a fresh turn instead of editing the previous one.
r1 = client.chat("My name is Alex")
r2 = client.chat("What is my name?", session_id=r1.session_id)
print(r2.response) # "Your name is Alex."
Note: Earlier versions of this library would overwrite the same message when you reused
session_idwithout an explicitparent_message_id. That is fixed — you no longer need to passparent_message_idmanually.
Usage Examples
Basic — default model
from deepseek import DeepSeekClient
client = DeepSeekClient(api_key="YOUR_TOKEN_HERE")
response = client.chat("Explain quantum computing")
print(response.response)
Expert model
response = client.chat(
"Solve this integral: ∫x²dx",
model="expert"
)
print(response.response)
Thinking enabled
response = client.chat(
"What is 17 * 23?",
thinking=True
)
print(response.full_response)
# Output:
# We need to compute 17 * 23. 17*20=340, 17*3=51, total is 391.
#
# 17 multiplied by 23 equals 391.
Web search enabled
response = client.chat(
"What happened in the news today?",
search=True
)
print(response.response)
Image attached
response = client.chat(
"Describe this photo in two sentences",
files=["vacation.jpg"]
)
print(response.response)
PDF attached + thinking
response = client.chat(
"What are the three main risks mentioned in this PDF?",
files=["annual_report.pdf"],
thinking=True,
)
print(response.full_response)
Multi-turn conversation about an uploaded file
file_id = client.upload_file("data.csv")
r1 = client.chat("Give me a 1-line summary of this CSV", file_ids=[file_id])
r2 = client.chat("Which column has the highest variance?", file_ids=[file_id], session_id=r1.session_id)
r3 = client.chat("Suggest 3 charts to visualise it", file_ids=[file_id], session_id=r1.session_id)
Expert + thinking
response = client.chat(
"Prove that sqrt(2) is irrational",
model="expert",
thinking=True
)
print(response.full_response)
Expert + web search
response = client.chat(
"Latest AI research papers in 2025",
model="expert",
search=True
)
print(response.response)
Default + thinking + web search
response = client.chat(
"What is today's weather in Mumbai?",
thinking=True,
search=True
)
print(response.full_response)
Expert + thinking + web search (all enabled)
response = client.chat(
"Analyze the current state of AI regulation globally",
model="expert",
thinking=True,
search=True
)
print(response.full_response)
Continue a conversation (reuse session)
response1 = client.chat("My name is Alex")
session_id = response1.session_id
response2 = client.chat("What is my name?", session_id=session_id)
print(response2.response)
Error Handling
from deepseek import DeepSeekClient, DeepSeekConnectionError, DeepSeekAPIError
client = DeepSeekClient(api_key="YOUR_TOKEN_HERE")
try:
response = client.chat("Hello!")
print(response.response)
except DeepSeekConnectionError as e:
print(f"Connection failed: {e}")
except DeepSeekAPIError as e:
print(f"API error: {e}")
| Exception | When it happens |
|---|---|
DeepSeekConnectionError |
Cannot connect to DeepSeek (network issue) |
DeepSeekAPIError |
Token expired/invalid, file failed to parse, or DeepSeek returned an error |
Telegram Bot Example — Full Feature Support
A complete, production-ready bot that supports every feature of p2d-deepseek:
- Text chat with per-user session memory
- Photo uploads (single + albums / media groups)
- Document uploads (PDFs, code, spreadsheets, etc.)
/think on|off— toggle reasoning mode/search on|off— toggle live web search/model default|expert— switch model/reset— start a new conversation/status— show current settings/start,/help- Long-message splitting (Telegram's 4096-char limit)
- Per-user state — every user has their own session, model, flags
Install dependencies first:
pip install p2d-deepseek python-telegram-bot
import asyncio
import os
import tempfile
from collections import defaultdict
from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
)
from deepseek import DeepSeekClient, DeepSeekConnectionError, DeepSeekAPIError
DEEPSEEK_TOKEN = "YOUR_DEEPSEEK_TOKEN"
BOT_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN"
client = DeepSeekClient(api_key=DEEPSEEK_TOKEN)
DEFAULT_STATE = {
"session_id": None,
"model": "default",
"thinking": False,
"search": False,
}
user_state = defaultdict(lambda: dict(DEFAULT_STATE))
album_buffer = defaultdict(list)
def get_state(uid: int) -> dict:
return user_state[uid]
async def _download(file_obj, suffix: str) -> str:
fd, path = tempfile.mkstemp(suffix=suffix)
os.close(fd)
await file_obj.download_to_drive(path)
return path
async def _send_long(msg, text: str):
if not text:
text = "(empty response)"
LIMIT = 4000
for i in range(0, len(text), LIMIT):
await msg.reply_text(text[i : i + LIMIT])
def _toggle(arg: str) -> bool:
return arg.strip().lower() in {"on", "true", "1", "yes", "y"}
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Hi! I am a DeepSeek bot.\n\n"
"Send me text, photos, or documents.\n\n"
"Commands:\n"
"/think on|off — toggle thinking mode\n"
"/search on|off — toggle web search\n"
"/model default|expert — switch model\n"
"/reset — start a new conversation\n"
"/status — show current settings\n"
"/help — show this message"
)
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
await cmd_start(update, context)
async def cmd_reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
s = get_state(update.effective_user.id)
s["session_id"] = None
await update.message.reply_text("Session cleared. Next message starts a new conversation.")
async def cmd_think(update: Update, context: ContextTypes.DEFAULT_TYPE):
s = get_state(update.effective_user.id)
if context.args:
s["thinking"] = _toggle(context.args[0])
else:
s["thinking"] = not s["thinking"]
await update.message.reply_text(f"Thinking: {'ON' if s['thinking'] else 'OFF'}")
async def cmd_search(update: Update, context: ContextTypes.DEFAULT_TYPE):
s = get_state(update.effective_user.id)
if context.args:
s["search"] = _toggle(context.args[0])
else:
s["search"] = not s["search"]
await update.message.reply_text(f"Web search: {'ON' if s['search'] else 'OFF'}")
async def cmd_model(update: Update, context: ContextTypes.DEFAULT_TYPE):
s = get_state(update.effective_user.id)
if context.args and context.args[0].lower() in {"default", "expert"}:
s["model"] = context.args[0].lower()
await update.message.reply_text(f"Model set to: {s['model']}")
else:
await update.message.reply_text("Usage: /model default | expert")
async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
s = get_state(update.effective_user.id)
await update.message.reply_text(
f"Model: {s['model']}\n"
f"Thinking: {'ON' if s['thinking'] else 'OFF'}\n"
f"Search: {'ON' if s['search'] else 'OFF'}\n"
f"Session: {'active' if s['session_id'] else 'new'}"
)
async def _process(uid: int, msg, prompt: str, file_paths: list):
s = get_state(uid)
try:
await msg.chat.send_action(ChatAction.TYPING)
def run():
return client.chat(
prompt,
model=s["model"],
thinking=s["thinking"],
search=s["search"],
session_id=s["session_id"],
files=file_paths or None,
)
response = await asyncio.to_thread(run)
s["session_id"] = response.session_id
await _send_long(msg, response.full_response)
except DeepSeekConnectionError:
await msg.reply_text("Connection error, please try again.")
except DeepSeekAPIError as e:
await msg.reply_text(f"API error: {e}")
except Exception as e:
await msg.reply_text(f"Unexpected error: {e}")
finally:
for p in file_paths:
try:
os.remove(p)
except OSError:
pass
async def _flush_album(context: ContextTypes.DEFAULT_TYPE):
job = context.job
uid, group_id = job.data
items = album_buffer.pop(group_id, [])
if not items:
return
items.sort(key=lambda x: x["mid"])
first_msg = items[0]["msg"]
prompt = next((it["caption"] for it in items if it["caption"]), "Describe these")
file_paths = []
for it in items:
try:
file_paths.append(await _download(it["tg_file"], it["suffix"]))
except Exception:
continue
await _process(uid, first_msg, prompt, file_paths)
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
msg = update.message
if not msg:
return
uid = update.effective_user.id
if msg.media_group_id:
if msg.photo:
tg_file = await msg.photo[-1].get_file()
suffix = ".jpg"
elif msg.document:
tg_file = await msg.document.get_file()
suffix = os.path.splitext(msg.document.file_name or "")[1] or ".bin"
else:
return
album_buffer[msg.media_group_id].append(
{"mid": msg.message_id, "msg": msg, "tg_file": tg_file, "suffix": suffix, "caption": msg.caption or ""}
)
for j in context.job_queue.get_jobs_by_name(f"album:{msg.media_group_id}"):
j.schedule_removal()
context.job_queue.run_once(
_flush_album, when=1.5, data=(uid, msg.media_group_id), name=f"album:{msg.media_group_id}"
)
return
prompt = msg.text or msg.caption or "Describe this"
file_paths = []
if msg.photo:
tg_file = await msg.photo[-1].get_file()
file_paths.append(await _download(tg_file, ".jpg"))
elif msg.document:
tg_file = await msg.document.get_file()
suffix = os.path.splitext(msg.document.file_name or "")[1] or ".bin"
file_paths.append(await _download(tg_file, suffix))
await _process(uid, msg, prompt, file_paths)
def main():
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("help", cmd_help))
app.add_handler(CommandHandler("reset", cmd_reset))
app.add_handler(CommandHandler("think", cmd_think))
app.add_handler(CommandHandler("search", cmd_search))
app.add_handler(CommandHandler("model", cmd_model))
app.add_handler(CommandHandler("status", cmd_status))
app.add_handler(
MessageHandler(
(filters.TEXT | filters.PHOTO | filters.Document.ALL) & ~filters.COMMAND,
handle_message,
)
)
app.run_polling()
if __name__ == "__main__":
main()
Notes
- Tokens expire when your DeepSeek session ends — refresh using any method above
- Web search slightly increases response time but adds real-time data
- Thinking mode exposes the model's internal reasoning before the final answer
- All responses are automatically stripped of HTML tags and citation markers
- Always use
response.full_responsein bots — it works correctly whether thinking is enabled or not, and avoids duplicate messages - File uploads are parsed by DeepSeek before the model can read them. The client polls until parsing is
SUCCESS(default 120 s timeout — increase viaupload_file(..., timeout=...)for very large files) - Upload a file once and reuse its
file_idfor follow-up questions instead of re-uploading every turn
License
MIT — made by addy
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file p2d_deepseek-0.1.9.tar.gz.
File metadata
- Download URL: p2d_deepseek-0.1.9.tar.gz
- Upload date:
- Size: 29.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3498580eeb90dfa07c5e1bebdd30f54ba0d3857da540084a1cdc8bb245a42bb8
|
|
| MD5 |
f6224b10741f4bed3579c9a449b879ba
|
|
| BLAKE2b-256 |
51fb6372897b1890fdc2a63fdc5416f30fa62bdd4f17113a922a7eb363c01c86
|
File details
Details for the file p2d_deepseek-0.1.9-py3-none-any.whl.
File metadata
- Download URL: p2d_deepseek-0.1.9-py3-none-any.whl
- Upload date:
- Size: 33.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea8b71870ffba90104b1d5bb4cd223bf4ba1d3b4114b7f5d0f80e64ba786925b
|
|
| MD5 |
0dcee21aeab6aab3f6aa99a395360139
|
|
| BLAKE2b-256 |
8ce0c9209910ffd5483b63408ba36436cdc2031a1b3a71b3595d10d675f8e86a
|