How to Build a DeepSeek Streamlit App in Python

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

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
pipavailable 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,uvorpoetry. We will usevenv. - 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
- Send “Reply with the single word: pong.” — you should see
pongstream in. - Switch the sidebar model to
deepseek-v4-proand 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. - 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.
- Click Clear conversation and confirm both history and the cost meter reset.
Step 5: Cost worked example

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

- Wire your app to Discord with the DeepSeek Discord bot tutorial — same SDK, different transport.
- Add memory and document retrieval with DeepSeek with LangChain.
- Swap the API for a local model using running DeepSeek on Ollama if you need offline inference.
- Browse other walkthroughs in the DeepSeek tutorials hub.
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
Primary sources
- OfficialDeepSeek API documentationEndpoints, request/response shape, model IDsLast checked: October 25, 2026
- RepositoryDeepSeek-AI on GitHubOpen-weight release details, training/inference notesLast checked: October 25, 2026
- Official documentationDeepSeek pricing pagePeak/off-peak rates per 1M tokens, peak hours, 1M context / 384K outputLast checked: October 25, 2026
- Official documentationDeepSeek API reference — Create Chat Completionstream_options.include_usage; usage on the last chunk; prompt_cache_hit_tokens / prompt_cache_miss_tokensLast checked: October 25, 2026
- Official documentationDeepSeek API docs — Thinking ModeThinking on by default; extra_body toggle; reasoning_effort; temperature ignored in thinking mode; reasoning_content ignored without tools, required with toolsLast checked: October 25, 2026
- Official documentationDeepSeek API docs — Rate Limit & IsolationConcurrency limits 2,500 / 500; HTTP 429Last checked: October 25, 2026
- Official announcementDeepSeek news — V4.1-Flash release (10 Sep 2026)Attributed Flash-vs-Pro statementLast checked: October 25, 2026
Technical references
- Tool documentationStreamlit docs — Secrets management.streamlit/secrets.toml; FileNotFoundError when no secrets file existsLast checked: October 25, 2026
- Tool documentationStreamlit docs — App dependencies for Community Cloudrequirements.txt; Streamlit installed by defaultLast checked: October 25, 2026
- Package registrystreamlit on PyPIVersion 1.66.1 requires Python 3.11+Last checked: October 25, 2026
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.
