How to Build a DeepSeek Streamlit App in Python

Build a DeepSeek Streamlit app with streaming chat, thinking mode and cost controls. Follow the step-by-step Python tutorial now.

Tutorials·April 25, 2026·By DS Guide Editorial
DeepSeek Streamlit app title card: eyebrow 'Tutorial · Streamlit', title 'Streamlit Chat App', tagline 'One-file chat UI for deepseek-flash with a cost meter.', and four stats: 2 API models, 3.11 minimum Python, 1M context tokens, and 50% off-peak vs peak, beside a compact-desktop illustration.

You want a working chat UI in front of DeepSeek without spending a weekend wiring up React, websockets and a backend. A DeepSeek Streamlit app gets you there in about 130 lines of Python: a text input, streamed responses, a model picker for DeepSeek’s two API models, and a running cost meter. That is the project this tutorial walks through end to end.

By the end you will have a single app.py that talks to deepseek-flash for default chat, can switch to deepseek-v4-pro, toggles thinking mode, streams tokens as they arrive, and shows the running cost per conversation. The code runs locally and deploys to Streamlit Community Cloud unchanged.

What you’ll build

Illustration: laptop browser at localhost:8501 showing a Streamlit chat titled 'DeepSeek Chat' with a 'V4' model dropdown, a temperature slider and a streaming reply about retrieval-augmented generation; a mock-up, not a capture of this tutorial's app.

The finished app is a single-file Streamlit chat that talks to DeepSeek’s OpenAI-compatible API. It supports both API models, a thinking-mode toggle, server-sent-events streaming, conversation persistence within the session, and a sidebar that totals dollars as you chat. Chat requests hit POST /chat/completions, the OpenAI-compatible endpoint at https://api.deepseek.com, so the official OpenAI Python SDK works unchanged once you swap base_url and api_key.

Two design choices matter up front. First, the DeepSeek API is stateless — the server does not remember prior turns, so the client has to resend the full messages array every request. Streamlit’s st.session_state is exactly the right place to keep that history. Second, DeepSeek’s API lists two models — deepseek-flash (served by DeepSeek-V4.1-Flash-1017, the default choice) and deepseek-v4-pro (served by DeepSeek-V4-Pro-0813) — and thinking is a request parameter, on by default, not a separate model ID. We will expose both choices in the sidebar.

Prerequisites

  • Python 3.11 or newer with pip available on your PATH (Streamlit 1.66 requires 3.11+).
  • A DeepSeek API key. If you do not have one yet, follow the get a DeepSeek API key walkthrough before continuing.
  • A virtual environment tool — venv, uv or poetry. We will use venv.
  • About 5 minutes and roughly $0.01 of API credit to try it end to end on deepseek-flash.
  • Optional but recommended: a free Streamlit Community Cloud account for deployment.

If this is your first time hitting the API at all, run through DeepSeek API getting started first — it covers authentication and a minimal curl call before you wrap anything in a UI.

Step 1: Create the project and install dependencies

Open a terminal and create a clean project folder. The commands below are bash; PowerShell users can swap the activation line.

mkdir deepseek-streamlit && cd deepseek-streamlit
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install streamlit openai

Two packages, nothing else. streamlit is the UI framework and openai is the official SDK we point at DeepSeek’s base URL. No client-side tokenizer is needed: the cost meter uses the token counts DeepSeek returns with each response. (For estimating a prompt before you send it, the DeepSeek token counter is handy.)

Step 2: Store the API key safely

Streamlit reads secrets from .streamlit/secrets.toml locally and from the app’s settings on Streamlit Community Cloud. Create the file:

mkdir -p .streamlit
cat > .streamlit/secrets.toml <<'EOF'
DEEPSEEK_API_KEY = "sk-your-key-here"
EOF
echo ".venv/" > .gitignore
echo ".streamlit/secrets.toml" >> .gitignore

The .gitignore entry matters: a committed secrets.toml puts the key in your Git history. If you prefer environment variables, export DEEPSEEK_API_KEY instead; the code below reads either source. (Streamlit raises a FileNotFoundError when you read st.secrets and no secrets file exists, which is why the key lookup catches it.)

Step 3: Write the chat app

Create app.py with the following Python. It covers model selection, the thinking toggle, streaming, history, and a cost meter that applies DeepSeek’s peak or off-peak rate for the current UTC hour.

import os
from datetime import datetime, timezone

import streamlit as st
from openai import OpenAI

# USD per 1M tokens at PEAK rates, from DeepSeek's pricing page (October 2026).
# Off-peak rates are exactly half. Update both if the pricing page changes.
PRICING = {
    "deepseek-flash":  {"in_hit": 0.006, "in_miss": 0.30, "out": 1.20},
    "deepseek-v4-pro": {"in_hit": 0.044, "in_miss": 1.32, "out": 3.96},
}


def is_peak(now: datetime) -> bool:
    """Peak: 01:00-04:00 and 06:00-10:00 UTC, Monday-Friday.
    Chinese public holidays are off-peak too; this check does not know them,
    so it overestimates cost on those days."""
    return now.weekday() < 5 and (1 <= now.hour < 4 or 6 <= now.hour < 10)


def get_api_key():
    try:
        return st.secrets["DEEPSEEK_API_KEY"]
    except (FileNotFoundError, KeyError):
        return os.environ.get("DEEPSEEK_API_KEY")


st.set_page_config(page_title="DeepSeek Chat", page_icon="·", layout="centered")
st.title("DeepSeek Streamlit App")

# --- Sidebar: model, thinking, temperature, cost ---
with st.sidebar:
    model = st.selectbox("Model", list(PRICING.keys()), index=0)
    thinking = st.toggle("Thinking mode", value=False,
                         help="Sends thinking enabled with reasoning_effort=high")
    temperature = st.slider("Temperature", 0.0, 2.0, 1.3, 0.1, disabled=thinking,
                            help="Non-thinking mode only. 0.0 code/math, 1.0 analysis, "
                                 "1.3 chat, 1.5 creative")
    max_tokens = st.number_input("Max output tokens", 256, 32768, 4096, 256)
    if st.button("Clear conversation"):
        st.session_state.messages = []
        st.session_state.cost = 0.0
        st.rerun()
    st.metric("Session cost (USD)", f"${st.session_state.get('cost', 0.0):.4f}")

# --- Client (OpenAI SDK pointed at DeepSeek) ---
api_key = get_api_key()
if not api_key:
    st.error("Set DEEPSEEK_API_KEY in .streamlit/secrets.toml or your environment.")
    st.stop()
client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")

# --- State ---
if "messages" not in st.session_state:
    st.session_state.messages = []
if "cost" not in st.session_state:
    st.session_state.cost = 0.0

# --- Replay prior turns ---
for m in st.session_state.messages:
    with st.chat_message(m["role"]):
        if m.get("reasoning_content"):
            with st.expander("Reasoning", expanded=False):
                st.markdown(m["reasoning_content"])
        st.markdown(m["content"])

# --- Handle new input ---
if prompt := st.chat_input("Ask DeepSeek anything"):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    # The API is stateless, so the full history goes out with every request.
    kwargs = {
        "model": model,
        "messages": st.session_state.messages,
        "max_tokens": int(max_tokens),
        "stream": True,
        "stream_options": {"include_usage": True},
    }
    if thinking:
        kwargs["reasoning_effort"] = "high"
        kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
    else:
        # Thinking is on by default, so switch it off for plain chat.
        kwargs["extra_body"] = {"thinking": {"type": "disabled"}}
        kwargs["temperature"] = temperature

    with st.chat_message("assistant"):
        reasoning_box = st.expander("Reasoning", expanded=False) if thinking else None
        reasoning_area = reasoning_box.empty() if reasoning_box else None
        placeholder = st.empty()
        full_text, reasoning_text, usage = "", "", None
        for chunk in client.chat.completions.create(**kwargs):
            if chunk.usage:
                usage = chunk.usage  # arrives on the last chunk
            if not chunk.choices:
                continue
            delta = chunk.choices[0].delta
            piece = getattr(delta, "reasoning_content", None)
            if piece and reasoning_area:
                reasoning_text += piece
                reasoning_area.markdown(reasoning_text)
            if delta.content:
                full_text += delta.content
                placeholder.markdown(full_text + "▌")
        placeholder.markdown(full_text)

    reply = {"role": "assistant", "content": full_text}
    if reasoning_text:
        # Without tools, DeepSeek ignores reasoning_content sent back in history.
        reply["reasoning_content"] = reasoning_text
    st.session_state.messages.append(reply)

    # --- Cost: all three buckets, at the rate for the current UTC hour ---
    if usage:
        rates = PRICING[model]
        factor = 1.0 if is_peak(datetime.now(timezone.utc)) else 0.5
        hit = getattr(usage, "prompt_cache_hit_tokens", 0) or 0
        miss = getattr(usage, "prompt_cache_miss_tokens", None)
        if miss is None:
            miss = usage.prompt_tokens - hit
        cost = factor * (hit * rates["in_hit"]
                         + miss * rates["in_miss"]
                         + usage.completion_tokens * rates["out"]) / 1_000_000
        st.session_state.cost += cost
        st.rerun()

A few things worth flagging in that code. We pass stream_options={"include_usage": True}; DeepSeek returns the request’s token usage on the last chunk of the stream — including prompt_cache_hit_tokens and prompt_cache_miss_tokens — which is what makes the cost meter use real counts rather than an estimate. Because thinking is on by default, the non-thinking path switches it off explicitly; only then does temperature take effect, which is why the slider is disabled while thinking is on. When thinking is enabled, the stream returns reasoning_content alongside the final content, and we render the reasoning in a collapsible expander so it does not dominate the UI. DeepSeek’s thinking-mode guide says reasoning_content sent back in a request without tools is ignored, so keeping it in the stored history is safe.

Step 4: Run it locally

From the project folder:

streamlit run app.py

Streamlit prints a local URL (usually http://localhost:8501) and opens it in your browser. Type a question, watch the response stream in token by token, and check that the sidebar cost ticks up. On deepseek-flash, a short conversation costs a fraction of a cent.

Verify it worked

  1. Send “Reply with the single word: pong.” — you should see pong stream in.
  2. Switch the sidebar model to deepseek-v4-pro and ask a multi-step question. Confirm the response arrives and the session cost rises faster than on Flash for similar token counts — Pro’s list rates are 3.3× Flash’s on output and 4.4× on cache-miss input.
  3. Toggle Thinking mode on and ask a planning question. The “Reasoning” expander should populate with the model’s reasoning, and the final answer should follow in the main message body.
  4. Click Clear conversation and confirm both history and the cost meter reset.

Step 5: Cost worked example

Flat diagram of a Streamlit DeepSeek chat app with a sidebar cost meter tracking input and output token spend.

Suppose your Streamlit app handles 1,000,000 chat calls per month against deepseek-flash, with a 2,000-token system prompt that DeepSeek’s context cache picks up after the first call, a 200-token user message each turn, and a 300-token reply. The math, with all three buckets enumerated at the rates on DeepSeek’s pricing page as of October 2026 (off-peak is half of peak):

Bucket Tokens Peak rate (per 1M) Peak cost Off-peak rate (per 1M) Off-peak cost
Input, cache hit 2,000,000,000 $0.006 $12.00 $0.003 $6.00
Input, cache miss 200,000,000 $0.30 $60.00 $0.15 $30.00
Output 300,000,000 $1.20 $360.00 $0.60 $180.00
Total $432.00 $216.00

Run the same workload on deepseek-v4-pro ($0.044 cache hit, $1.32 cache miss and $3.96 output per 1M tokens at peak; $0.022, $0.66 and $1.98 off-peak) and the total becomes $88.00 + $264.00 + $1,188.00 = $1,540.00 entirely in peak hours, or $44.00 + $132.00 + $594.00 = $770.00 entirely off-peak. Peak hours are 01:00–04:00 and 06:00–10:00 UTC, Monday to Friday, excluding Chinese public holidays; a real app usually straddles both periods. Verify rates against the DeepSeek API pricing page before quoting them to anyone. For larger sweeps the DeepSeek pricing calculator is faster than rebuilding the table by hand.

Note that each new user message is a cache miss against the cached prefix, even when the system prompt is fully cached — that is why the uncached-input line is non-zero.

Step 6: Add useful extras

JSON mode for structured output

If you want the assistant to return structured data — a form extraction, a classification result — set response_format={"type": "json_object"} in the request kwargs. DeepSeek’s JSON mode is designed to return valid JSON, not guaranteed: it can occasionally return empty content, the prompt must include the word “json” plus a small example schema, and you should set max_tokens high enough to avoid truncation. Always wrap json.loads in a try/except and fall back gracefully.

Tool calling

The app above declares no tools, but DeepSeek supports OpenAI-format function calling on both models in thinking and non-thinking mode. Pass a tools=[...] list and handle tool_calls deltas in the stream. In thinking mode, a request that carries tools must send back the reasoning_content of every earlier assistant turn, or the API returns a 400 error; the history format above keeps it for every turn that ran with thinking on. For a fuller pattern, the DeepSeek Python integration tutorial covers tool dispatch end to end.

RAG over your own documents

To let users chat with their own files, layer a retrieval step before the chat call: embed documents, query a vector store, and inject the top chunks into the system message. The DeepSeek RAG tutorial walks through that pattern with the same OpenAI-SDK pointer at DeepSeek’s base URL.

Common errors and fixes

Symptom Likely cause Fix
AuthenticationError: 401 Missing or wrong API key Re-check .streamlit/secrets.toml; confirm the key starts with sk- and was copied without trailing whitespace.
Error that mentions deepseek-chat or deepseek-reasoner Code copied from an older tutorial Switch to deepseek-flash. The legacy IDs were scheduled for retirement on 2026-07-24 at 15:59 UTC and are no longer listed in DeepSeek’s docs.
Streaming hangs, no tokens render Streamlit placeholder.markdown not called inside the loop Confirm you are calling placeholder.markdown(full_text + "▌") inside the for-loop, not after it.
Temperature changes do nothing Thinking mode is on (the API default) Send extra_body={"thinking": {"type": "disabled"}}; temperature has no effect in thinking mode.
JSON mode returns empty content Prompt lacks the word “json” or max_tokens too low Add an example schema in the prompt; raise max_tokens; wrap parsing in try/except.
Cost meter stays at $0.0000 No usage read from the stream Check chunk.usage before skipping chunks; DeepSeek puts usage on the last chunk, which still has one choice with a finish_reason.
429 errors under load More concurrent requests than your account’s limit (2,500 for deepseek-flash, 500 for deepseek-v4-pro) Add exponential backoff; review DeepSeek API rate limits.

Deploying to Streamlit Community Cloud

Push the project to a public or private GitHub repository (without secrets.toml), add a requirements.txt that lists openai (Community Cloud installs Streamlit by default; list it too if you want to pin a version), connect the repo on share.streamlit.io, point it at app.py, and paste your DEEPSEEK_API_KEY into the app’s Secrets settings in the same TOML format. Because the API is stateless and history lives in st.session_state, each user gets their own conversation without any backend work on your side — but note that session state disappears when the session ends or the app restarts. For persistent history you would add a database; for most internal tools that is overkill.

Why these defaults

The temperature slider defaults to 1.3 because that is DeepSeek’s documented setting for general conversation and translation. Drop it to 0.0 for code generation or maths, leave it at 1.0 for data analysis, and push to 1.5 for creative writing — in non-thinking mode only. The default model is deepseek-flash because deepseek-v4-pro costs 3.3× as much on output, and DeepSeek’s 10 September 2026 release note says tests by multiple parties put V4.1-Flash ahead of V4-Pro on performance, cost, speed and total runtime. Keep DeepSeek V4-Pro in the picker for comparisons on your own tasks.

The 1M-token context window is the same on both models, with output up to 384K tokens, so you can paste a whole codebase into a turn without architecture changes. Most chat UIs never come close to that ceiling; if yours does, lean on context caching and check token counts before sending.

Next steps

Architecture diagram of a Streamlit Python app calling DeepSeek V4 and rendering replies in a browser.

Frequently asked questions

How do I connect Streamlit to the DeepSeek API?

Use the official OpenAI Python SDK and point it at DeepSeek’s base URL: OpenAI(base_url="https://api.deepseek.com", api_key=...). Chat requests hit POST /chat/completions with the same wire format as OpenAI, so no call sites change. Store the key in .streamlit/secrets.toml and read it with st.secrets, or from the DEEPSEEK_API_KEY environment variable. The full setup is covered in the DeepSeek OpenAI SDK compatibility guide.

What does a DeepSeek Streamlit app cost to run?

As of October 2026, deepseek-flash costs $0.30 per 1M cache-miss input tokens, $0.006 per 1M cache-hit tokens and $1.20 per 1M output tokens at peak, and half that off-peak ($0.15, $0.003 and $0.60). A typical short chat costs a fraction of a cent. deepseek-v4-pro costs 3.3× as much on output. Always enumerate all three token buckets when budgeting; the DeepSeek API pricing page has the full table.

Can I enable thinking mode in a Streamlit chat?

Yes — it is on by default. Thinking is a request parameter on either model, not a separate model. Pass reasoning_effort="high" (or "low" / "max") with extra_body={"thinking": {"type": "enabled"}}, or {"type": "disabled"} to turn it off. With thinking on, the stream returns reasoning_content alongside the final content, which you can render in a Streamlit expander. See DeepSeek V4 for background on the model family.

Why does my app forget previous messages between requests?

The DeepSeek API is stateless — the server does not remember prior turns, so the client must resend the full conversation in the messages array on every request. Streamlit’s st.session_state is the right place to hold history during a session. The web chat behaves differently because its UI persists state for you. More on the underlying API in the DeepSeek API documentation.

Is it safe to deploy a DeepSeek Streamlit app publicly?

Yes, with the usual caveats: keep the API key in Streamlit secrets and never in the repo, add per-user rate limiting if the URL is public, and review what data your users will send before deploying. DeepSeek processes requests on servers subject to Chinese law, so route sensitive data accordingly. The DeepSeek privacy guide covers the trade-offs.

Sources & Methodology

Last source check: October 25, 2026

Primary sources

Technical references

Methodology

Information in this article was checked against the official DeepSeek documentation, the article's cited primary sources, and the deepseekai.guide pricing dataset on the date shown above. Where third-party numbers are quoted, the originating source is linked inline so the reader can verify and re-check.

Data confidence

High for facts cited inline with primary-source links; medium for context narrative.

Editorial note

AI tooling moves quickly. If you spot an outdated number or a broken link, the contact link in the footer is the fastest way to flag it for the editorial team.

Leave a Reply

Your email address will not be published. Required fields are marked *