Skip to main content

Unofficial Python client for DeepSeek — supports default & expert models, thinking, web search, and file/image uploads.

Project description

p2d-deepseek

Telegram Channel PyPI Version Python Versions License

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-deepseek but the Python import name is deepseek. Install with pip install p2d-deepseek, then use from deepseek import DeepSeekClient in 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)

  1. Go to chat.deepseek.com and log in
  2. Open DevTools — press F12 or right-click anywhere → Inspect
  3. Go to the Application tab (if hidden, click » to find it)
  4. In the left sidebar, expand Local Storage → click https://chat.deepseek.com
  5. Find the key called userToken
  6. Copy the value field — that is your token

Method 2 — Network Tab (Desktop)

  1. Go to chat.deepseek.com and log in
  2. Open DevTools → Network tab
  3. Send any message in the chat
  4. Click on any request going to chat.deepseek.com
  5. Open the Headers section
  6. Find the authorization header and copy its value (without the Bearer prefix)

Method 3 — Kiwi Browser (Android / Mobile)

  1. Install Kiwi Browser from the Play Store
  2. Open chat.deepseek.com and log in
  3. Tap the menu (⋮) → Developer Tools
  4. Go to the Application tab → Local Storagehttps://chat.deepseek.com
  5. Find the key called userToken and 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_id without an explicit parent_message_id. That is fixed — you no longer need to pass parent_message_id manually.


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_response in 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 via upload_file(..., timeout=...) for very large files)
  • Upload a file once and reuse its file_id for 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

p2d_deepseek-0.1.9.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

p2d_deepseek-0.1.9-py3-none-any.whl (33.3 kB view details)

Uploaded Python 3

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

Hashes for p2d_deepseek-0.1.9.tar.gz
Algorithm Hash digest
SHA256 3498580eeb90dfa07c5e1bebdd30f54ba0d3857da540084a1cdc8bb245a42bb8
MD5 f6224b10741f4bed3579c9a449b879ba
BLAKE2b-256 51fb6372897b1890fdc2a63fdc5416f30fa62bdd4f17113a922a7eb363c01c86

See more details on using hashes here.

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

Hashes for p2d_deepseek-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 ea8b71870ffba90104b1d5bb4cd223bf4ba1d3b4114b7f5d0f80e64ba786925b
MD5 0dcee21aeab6aab3f6aa99a395360139
BLAKE2b-256 8ce0c9209910ffd5483b63408ba36436cdc2031a1b3a71b3595d10d675f8e86a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page