DeepSeek upgraded V4.1 Flash to the V4.1-Flash-1017 checkpoint on October 17, 2026, and API users did not have to touch their code. The model ID stays deepseek-flash and now points at the new build, according to MarkTechPost’s report on the release. Anything pinned to a version suffix or to the old deepseek-v4-pro name is the part of your stack that can break.
The checkpoint landed about five weeks after the original V4.1 Flash launch, which also folded V4 Pro traffic into Flash (see the coverage of the V4.1 Flash launch and V4 Pro sunset). Tutorials written in early September already carry stale model names, stale prices, and no mention of the peak and off-peak rate split that now drives the monthly bill.
Python remains the default language for this work. Stack Overflow’s 2025 developer survey found that 57.9% of respondents used Python for extensive development in the past year, up 7 percentage points from 2024, and that 84% of surveyed developers used or planned to use AI tools in their workflow (survey data). A DeepSeek client with streaming, validated JSON output, tool calling, retries, and a cost ledger fits in under 500 lines of Python, tests included. Twelve steps and about 45 minutes take you from an empty folder to that project.
Don't miss new tech stories on Google
Add Shattered once in the Google app and our stories show up in your news suggestions.
Where the DeepSeek API Stands on October 22, 2026 #
Three dates define the current state of the DeepSeek API. On September 10, 2026, DeepSeek launched V4.1-Flash with native multimodal support and a new price list that took effect at 04:00 UTC the same day, per the company’s release post. On September 14, requests addressed to deepseek-v4-pro began routing to V4.1 Flash and billing at Flash rates, a routing that DeepSeek said would continue until V4.1-Pro launches. On October 17, the Flash model moved to the 1017 build and the company opened its Search and Fetch APIs to outside developers.
The practical consequence: there is exactly one model alias a new project should call. DeepSeek’s changelog tells developers to use deepseek-flash for the latest Flash model, and the API calling method and the price framework did not change with the October update. The version-specific name, DeepSeek-V4.1-Flash-1017, describes the backend checkpoint rather than a separate model ID. Treat it as a label for release notes and never as a string in your request body.
Quality moved too. The 1017 build is described as faster and leaner, with gains in reasoning, coding, agentic tasks, and output length. Independent numbers are still arriving, and the early benchmark readings are collected in our look at the HLE score for V4.1-Flash-1017. For a tutorial the benchmark race matters less than the stability of the interface, and on that front the news is good: one alias, one endpoint, unchanged calling method.
Reliability deserves a sober note. Secondary reports described service slowdowns and an outage of roughly five hours in the days before the 1017 release, and those reports did not come from DeepSeek’s own changelog. Whatever the exact figure, any production client needs retries with backoff, timeouts, and a plan for a provider that is down. Steps 3 and 11 build exactly that.
| Date (2026) | Change | What it means for your code |
|---|---|---|
| September 10 | V4.1-Flash launches with native multimodal support and new pricing from 04:00 UTC | Peak and off-peak rates enter your cost math |
| September 14 | deepseek-v4-pro requests route to V4.1 Flash at Flash rates until V4.1-Pro launches |
Old Pro model IDs no longer return a Pro model |
| October 17 | V4.1-Flash-1017 replaces the earlier checkpoint, alias deepseek-flash unchanged |
No code change needed if you use the alias |
| October 17 | Search API and Fetch API open to all developers at $0.75 per 1,000 successful requests | New tool surface, billing starts November 1 |
| October 19 | Cloudflare lists the model on Workers AI | A second route to the same model family |
| October 20 | US administration bars DeepSeek from civilian federal agency systems, per CNBC reporting | Compliance check for government work |
| November 1 | Per-request billing for Search and Fetch begins at 00:00 Beijing time | Add a spending cap before this date |
One more status item. DeepSeek’s notices give no V4.1-Pro release date, so treat Pro as announced in principle and unavailable today. If a Pro tier ships, expect a new model alias and a new rate card, and expect the routing from deepseek-v4-pro to end. A config-driven model name, which Step 3 sets up, turns that future migration into a one-line change.
Prerequisites: Versions, Accounts and Budget #
Everything below was run against a local mock of the chat-completions endpoint, since a live key was not needed to verify the client logic. The mock returns streaming chunks, tool calls, JSON-mode replies, and usage fields with cache counters. That setup exercised every code path in the project. Live behavior, including exact token counts and latency, will differ, so treat the sample outputs as shape references.
- Python 3.11 or newer. The code uses a union type hint (
datetime | None), so use a recent interpreter. - pip 23 or newer inside a virtual environment, so nothing touches your system Python.
- openai (the official Python SDK). DeepSeek’s endpoint has followed the OpenAI chat-completions request format in earlier releases, and the code depends on that. Confirm against the API docs page titled Your First API Call before shipping.
- pydantic 2.x for validating JSON output,tenacity for retries,python-dotenv for local configuration, andpytest for tests.
- A DeepSeek platform account with an API key and a small prepaid balance. Every live request in this project costs a fraction of a cent, so a few dollars covers all twelve steps with room for experiments.
- A terminal and a text editor. Linux, macOS, and Windows all work. Windows users should run the PowerShell variants of the activation and environment commands shown in Steps 1 and 2.
Budget about 45 minutes. Steps 1 and 2 take ten minutes including account setup. Steps 3 through 8 are the core build and take about 25. Steps 9 through 12 add the cost ledger, command-line interface, tests, and final run, and fit in the remaining ten. Skip nothing in Step 1, because the key-handling habits there protect you from the most common and most expensive mistake: a leaked key.
DeepSeek API Pricing: Peak, Off-Peak and Cache Math #
Before writing code, read the rate card, because it shapes the architecture. V4.1 Flash pricing has two tiers. Off-peak rates are 50% of peak rates, according to DeepSeek’s pricing notice, and peak applies Monday through Friday from 01:00 to 04:00 UTC and from 06:00 to 10:00 UTC, excluding Chinese public holidays. The September launch brought the output price cut into effect, and the October 17 update kept the same price framework.
| Item | Off-peak (USD per 1M tokens) | Peak (USD per 1M tokens) |
|---|---|---|
| Input, cache hit | $0.003 | $0.006 |
| Input, cache miss | $0.15 | $0.30 |
| Output | $0.60 | $1.20 |
| Search API call | $0.75 per 1,000 successful requests | same, billing from November 1 |
| Fetch API call | $0.75 per 1,000 successful requests | same, billing from November 1 |
Cache hits cost far less than cache misses at the same time of day (0.003 against 0.15 off-peak). That gap is the single biggest lever on a DeepSeek bill. A prompt that begins with a long, unchanging system message and examples, followed by the variable user text, gives the provider’s context cache the best chance to hit. Step 9 teaches the ledger to track hits and misses separately so you can see the effect in your own numbers rather than trusting a rule of thumb.
The scenarios below use only the published rates and plain arithmetic. A request with 1,500 uncached input tokens and 400 output tokens costs $0.000225 for input plus $0.00024 for output, or $0.000465 off-peak. The same request at peak costs $0.00093. At 10,000 such requests a day the figures climb into real money, which is why scheduling matters.
| Scenario | Off-peak | Peak | Blended (uniform load) |
|---|---|---|---|
| One request, 1,500 in (uncached) and 400 out | $0.000465 | $0.00093 | n/a |
| 10,000 requests a day, no cache hits | $4.65 | $9.30 | Not listed |
| 10,000 requests a day, 60% of input cached | Not listed | Not listed | Not listed |
| 10,000 Search calls (from Nov 1) | $7.50 | $7.50 | $7.50 |
| 30 days, 10,000 requests a day, no cache, all off-peak | Not listed | n/a | n/a |
| 30 days, same load, uniform spread, 60% cached | n/a | n/a | Not listed |
The blended column assumes load spreads evenly across the week. Peak hours cover 5 out of every 24 hours on average (7 hours on each of 5 weekdays, out of 168 hours), so blended cost equals the off-peak cost plus 5/24 of the gap between peak and off-peak. The estimate ignores Chinese public holidays, which DeepSeek excludes from peak, so real bills can land slightly lower.
Build the Foundation: Steps 1 to 4 #
Step 1: Create an API Key and Lock It Down
Sign in to the DeepSeek platform console, open the API keys section, and create a key with a descriptive name such as deepseek-lab-dev. The console shows the secret once. Copy it straight into a password manager, because a lost key cannot be recovered and has to be replaced. Add a small prepaid balance, since requests fail with a billing error when the balance hits zero.
Create one key per project and per environment. A single shared key makes rotation painful, and it hides which service is spending money. If the platform offers spending limits or usage alerts, switch them on now. A $5 alert threshold catches a runaway loop within minutes, while a monthly invoice catches it after the damage is done.
Never paste the key into source code, a notebook cell, or a chat message. Store it in an environment variable for local work and in your platform’s secret manager for deployment. The project reads it from a .env file that Git ignores, and the file permissions should block other users on the machine.
DEEPSEEK_API_KEY=sk-your-key-here
DEEPSEEK_MODEL=deepseek-flash
chmod 600 .env
$env:DEEPSEEK_API_KEY = "sk-your-key-here"
Step 2: Create the Project and Install Dependencies
A virtual environment keeps the dependencies isolated and makes the project reproducible on a teammate’s machine or a build server. The commands below create the folder, activate the environment, and install the five packages. The empty __init__.py turns the dslab folder into an importable package, which is what lets you run modules with python -m dslab.cli later.
mkdir deepseek-lab && cd deepseek-lab
python3 -m venv .venv
source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install openai tenacity "pydantic>=2" python-dotenv pytest
mkdir dslab tests && touch dslab/__init__.py
printf '.env\n.venv/\n__pycache__/\n' > .gitignore
pip freeze | grep -iE "^(openai|tenacity|pydantic|python-dotenv|pytest)=="
The last command prints the pinned versions you ended up with. Copy them into requirements.txt once everything works, so a later install reproduces your exact setup. The .gitignore line matters more than it looks: it is the guardrail between your key and a public repository.
Step 3: Add Configuration and a Retrying Client
Two small files carry most of the production value. config.py reads the key, base URL, and model name from the environment, with deepseek-flash as the default model. Keeping the model name in configuration means the September-style surprises (a routed alias, a retired name) become a one-line change instead of a code hunt.
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
@dataclass(frozen=True)
class Settings:
api_key: str
base_url: str = "https://api.deepseek.com"
model: str = "deepseek-flash"
timeout: float = 60.0
def load_settings() -> Settings:
key = os.getenv("DEEPSEEK_API_KEY", "").strip()
if not key:
raise SystemExit("DEEPSEEK_API_KEY is not set. Add it to .env or export it.")
return Settings(
api_key=key,
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
model=os.getenv("DEEPSEEK_MODEL", "deepseek-flash"),
)
client.py builds the SDK client and wraps the call in a retry policy. The SDK has its own retry setting, so the code switches it off with max_retries=0 and lets tenacity own the policy. Two retry layers multiply into long, confusing stalls. The policy retries only errors that a retry can fix (rate limits, connection failures, timeouts, and 5xx responses) and gives up after five attempts with jittered exponential backoff capped at 30 seconds.
import openai
from openai import OpenAI
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
from .config import Settings
RETRYABLE = (
openai.RateLimitError,
openai.APIConnectionError,
openai.APITimeoutError,
openai.InternalServerError,
)
def make_client(settings: Settings) -> OpenAI:
return OpenAI(
api_key=settings.api_key,
base_url=settings.base_url,
timeout=settings.timeout,
max_retries=0,
)
retry_policy = retry(
retry=retry_if_exception_type(RETRYABLE),
wait=wait_random_exponential(multiplier=1, max=30),
stop=stop_after_attempt(5),
reraise=True,
)
@retry_policy
def chat_once(client: OpenAI, settings: Settings, messages: list, **kwargs):
return client.chat.completions.create(
model=settings.model, messages=messages, **kwargs
)
Authentication errors, bad requests, and insufficient-balance errors are deliberately absent from the retry list. Repeating a request with a bad key just burns time. Those errors should surface immediately so a human can fix the cause.
Step 4: Make Your First DeepSeek API Call
A ten-line script confirms the key, the endpoint, and the alias in one shot. Save it as hello.py in the project root and run it with the environment active.
from dslab.client import chat_once, make_client
from dslab.config import load_settings
settings = load_settings()
client = make_client(settings)
resp = chat_once(
client,
settings,
[{"role": "user", "content": "In one sentence, what is a mixture-of-experts model?"}],
)
print(resp.choices[0].message.content)
print(resp.usage)
Run python hello.py. The reply and the usage object should look like the example below. Your wording and counts will differ, and the usage object may carry extra fields, including cache counters, which Step 9 reads.
$ python hello.py
A mixture-of-experts model splits its parameters into many specialist sub-networks and
routes each token to only a few of them, so it can be huge in total size yet cheap to run per token.
CompletionUsage(completion_tokens=41, prompt_tokens=17, total_tokens=58)
If the call fails, jump to the troubleshooting section. If it works, resist the urge to hard-code anything you saw in the response, including the model field the server echoes back. Server-side names can change while your alias stays valid, and the earlier V4 Pro 0813 release showed how quickly a Pro-tier name and its pricing can shift under a project that pinned them.
Add the Core Features: Steps 5 to 8 #
Step 5: Stream Responses Token by Token
Streaming cuts perceived latency from the full generation time to the time of the first token. For chat interfaces and command-line tools, that difference decides whether the app feels alive or frozen. The OpenAI-style streaming protocol sends small chunks, each carrying a fragment of text in choices[0].delta.content.
The trap is billing visibility. Without an extra option, a streamed response does not report token usage, and your cost ledger stays blind. Passing stream_options={"include_usage": True} asks the server to append one final chunk that has an empty choices list and a populated usage field. The loop in chat.py handles both kinds of chunk, and it guards against empty choices so the final usage chunk never raises an IndexError. If your gateway ignores the option, the ledger simply reports zeros and you will know to check.
Step 6: Keep Conversation Memory Without Blowing the Budget
The chat API is stateless. Every request must carry the history it needs, which means cost grows with each turn, because turn 10 re-sends turns 1 through 9 as input. The Conversation class keeps a system message that never changes, a list of turns, and a sliding window of the most recent max_turns exchanges. A window of 12 exchanges is generous for a support bot and wasteful for a quick-answer tool, so tune it to the job.
import sys
from .client import chat_once
from .pricing import Ledger
class Conversation:
def __init__(self, client, settings, system: str, ledger: Ledger, max_turns: int = 12):
self.client = client
self.settings = settings
self.ledger = ledger
self.system = {"role": "system", "content": system}
self.turns: list[dict] = []
self.max_turns = max_turns
def _messages(self) -> list[dict]:
return [self.system, *self.turns[-self.max_turns * 2 :]]
def ask(self, text: str, stream: bool = True, out=sys.stdout) -> str:
self.turns.append({"role": "user", "content": text})
if stream:
answer = self._stream(out)
else:
resp = chat_once(self.client, self.settings, self._messages(), temperature=0.3)
self.ledger.add(resp.usage)
answer = resp.choices[0].message.content or ""
self.turns.append({"role": "assistant", "content": answer})
return answer
def _stream(self, out) -> str:
parts: list[str] = []
stream = chat_once(
self.client,
self.settings,
self._messages(),
temperature=0.3,
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.usage:
self.ledger.add(chunk.usage)
if chunk.choices and chunk.choices[0].delta.content:
piece = chunk.choices[0].delta.content
parts.append(piece)
out.write(piece)
out.flush()
out.write("\n")
return "".join(parts)
Two design choices deserve a comment. First, the system message sits at the front and never changes between calls. Context caching works on repeated prefixes in DeepSeek’s earlier releases, so a stable opening gives the cache a chance to hit and moves tokens from the 0.15 column to the 0.003 column of the off-peak rate card. Second, trimming old turns shifts the window start, which changes the prefix after the system message. If cache hits matter more than memory depth, summarize old turns into one stable block instead of dropping them one by one.
Step 7: Get Validated JSON Output
Free-form text cannot feed a database or an API. JSON mode asks the model to return a single JSON object, and DeepSeek has supported it through the response_format parameter with {"type": "json_object"}. Two habits make it dependable. The prompt must say the word JSON and show the schema, and the code must validate whatever comes back, because JSON mode guarantees syntax at best and never guarantees your schema.
import json
import re
from typing import Literal
from pydantic import BaseModel, ValidationError
from .client import chat_once
class Review(BaseModel):
summary: str
risks: list[str]
severity: Literal["low", "medium", "high"]
SYSTEM = (
"You review short technical notes. Reply with one JSON object and nothing else. "
'Schema: {"summary": string, "risks": [string], "severity": "low" | "medium" | "high"}'
)
def _strip_fences(text: str) -> str:
text = text.strip()
match = re.match(r"^```(?:json)?\s*(.*?)\s*```$", text, flags=re.S)
return match.group(1) if match else text
def extract_review(client, settings, ledger, note: str) -> Review:
messages = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": note},
]
last_error = ""
for attempt in range(2):
resp = chat_once(
client,
settings,
messages,
temperature=0,
response_format={"type": "json_object"},
)
ledger.add(resp.usage)
raw = resp.choices[0].message.content or ""
try:
return Review.model_validate_json(_strip_fences(raw))
except (ValidationError, json.JSONDecodeError) as exc:
last_error = str(exc)
messages.append({"role": "assistant", "content": raw})
messages.append(
{
"role": "user",
"content": f"That JSON failed validation: {last_error[:300]}. Return corrected JSON only.",
}
)
raise ValueError(f"Model never returned valid JSON: {last_error[:200]}")
Pydantic does the schema check. When validation fails, the loop appends the bad reply and the error message to the conversation and asks for a corrected object, once. One repair attempt fixes most formatting slips cheaply, and a hard cap prevents an endless loop of paid retries. The _strip_fences helper handles a common quirk where a model wraps JSON in a Markdown code fence even when told not to. Temperature 0 keeps extraction deterministic enough to test.
Step 8: Add Tool Calling
Tool calling turns the model into a controller. You describe functions with JSON Schema, the model decides when to call one, your code runs it, and you send the result back for a final answer. DeepSeek’s materials stress agentic ability in the Flash line, and the Flash-1017 agent benchmark results suggest the 1017 build leans further into that. The loop below is the same shape any agent framework uses underneath.
import json
from datetime import datetime, timezone
from .client import chat_once
from .pricing import cost_usd
TOOLS = [
{
"type": "function",
"function": {
"name": "utc_now",
"description": "Return the current UTC time as an ISO 8601 string.",
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"function": {
"name": "estimate_cost",
"description": "Estimate the USD cost of one deepseek-flash call at the current peak or off-peak rate, assuming no cache hits.",
"parameters": {
"type": "object",
"properties": {
"input_tokens": {"type": "integer", "minimum": 0},
"output_tokens": {"type": "integer", "minimum": 0},
},
"required": ["input_tokens", "output_tokens"],
},
},
},
]
def run_tool(name: str, args: dict) -> dict:
if name == "utc_now":
return {"utc": datetime.now(timezone.utc).isoformat(timespec="seconds")}
if name == "estimate_cost":
usd = cost_usd(0, int(args["input_tokens"]), int(args["output_tokens"]))
return {"usd": round(usd, 8)}
return {"error": f"unknown tool {name}"}
def chat_with_tools(client, settings, ledger, question: str, max_rounds: int = 4) -> str:
messages = [
{"role": "system", "content": "Use the provided tools for time and cost questions. Be brief."},
{"role": "user", "content": question},
]
for _ in range(max_rounds):
resp = chat_once(client, settings, messages, tools=TOOLS, temperature=0)
ledger.add(resp.usage)
msg = resp.choices[0].message
if not msg.tool_calls:
return msg.content or ""
messages.append(
{
"role": "assistant",
"content": msg.content,
"tool_calls": [
{
"id": c.id,
"type": "function",
"function": {"name": c.function.name, "arguments": c.function.arguments},
}
for c in msg.tool_calls
],
}
)
for call in msg.tool_calls:
try:
args = json.loads(call.function.arguments or "{}")
result = run_tool(call.function.name, args)
except (ValueError, KeyError, TypeError) as exc:
result = {"error": str(exc)}
messages.append(
{"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}
)
raise RuntimeError(f"Tool loop did not finish in {max_rounds} rounds")
Four details keep the loop safe. The tool list is explicit, so the model can only call functions you wrote. Arguments arrive as a JSON string and can be malformed, so the parser sits inside a try block that returns an error object to the model instead of crashing. The loop stops after four rounds, because a confused model can call tools forever at your expense. And the assistant message is rebuilt by hand from the tool calls, which avoids sending unexpected extra fields back to the endpoint.
Both demo tools run locally and touch nothing sensitive. When you replace them with real ones (a database lookup, a ticket creator, a web fetch), apply the same rules you would use for any public API: validate every argument, enforce an allowlist, and never let model output flow straight into a shell command or a SQL string.
Ship It: Steps 9 to 12 #
Step 9: Track Cost With Peak and Off-Peak Awareness
A ledger that multiplies tokens by one flat price is wrong half the time. The pricing.py module encodes the rate card from the table earlier, checks whether the current UTC moment falls in a peak window, and prices cache hits, cache misses, and output separately. The Ledger class reads the cache counters from the usage object with getattr and falls back to treating all input as a miss when the counters are absent. DeepSeek’s usage object has reported prompt_cache_hit_tokens and prompt_cache_miss_tokens in earlier releases, and the fallback keeps the code safe if that changes.
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass(frozen=True)
class Rates:
cache_hit: float
cache_miss: float
output: float
OFF_PEAK = Rates(cache_hit=0.003, cache_miss=0.15, output=0.60)
PEAK = Rates(cache_hit=0.006, cache_miss=0.30, output=1.20)
PEAK_WINDOWS = ((1, 4), (6, 10))
def is_peak(now: datetime | None = None) -> bool:
now = now or datetime.now(timezone.utc)
if now.weekday() >= 5:
return False
return any(start <= now.hour < end for start, end in PEAK_WINDOWS)
def cost_usd(hit: int, miss: int, out: int, now: datetime | None = None) -> float:
r = PEAK if is_peak(now) else OFF_PEAK
return (hit * r.cache_hit + miss * r.cache_miss + out * r.output) / 1_000_000
@dataclass
class Ledger:
calls: int = 0
hit: int = 0
miss: int = 0
out: int = 0
usd: float = 0.0
def add(self, usage, now: datetime | None = None) -> None:
if usage is None:
return
hit = getattr(usage, "prompt_cache_hit_tokens", None) or 0
miss = getattr(usage, "prompt_cache_miss_tokens", None)
if miss is None:
miss = max((usage.prompt_tokens or 0) - hit, 0)
out = usage.completion_tokens or 0
self.calls += 1
self.hit += hit
self.miss += miss
self.out += out
self.usd += cost_usd(hit, miss, out, now)
def summary(self) -> str:
total_in = self.hit + self.miss
ratio = (self.hit / total_in * 100) if total_in else 0.0
return (
f"{self.calls} call(s) | in {total_in} tok ({ratio:.0f}% cached) | "
f"out {self.out} tok | est. ${self.usd:.6f}"
)
The module bakes in two simplifications, both on the safe side for budgeting. It does not model Chinese public holidays, which DeepSeek excludes from peak, so it may overstate cost on those days. And it prices the call at the moment the response arrives, so a request that straddles a window boundary gets the later tier. Treat the ledger as an estimate to compare against the invoice, never as the invoice.
Step 10: Wrap Everything in a Command-Line Interface
A CLI turns the modules into something you can demo, script, and put in a cron job. argparse subcommands cover the four workflows: interactive chat, JSON extraction, tool calling, and a rate command that prints whether the peak or off-peak tier applies right now. Every command ends by printing the ledger summary, so each run shows what it cost.
import argparse
from .chat import Conversation
from .client import make_client
from .config import load_settings
from .pricing import Ledger, is_peak
from .structured import extract_review
from .tools import chat_with_tools
def main() -> None:
parser = argparse.ArgumentParser(prog="dslab", description="DeepSeek API lab")
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("chat", help="interactive streaming chat")
p_ext = sub.add_parser("extract", help="structured JSON review of a note")
p_ext.add_argument("note")
p_tool = sub.add_parser("tools", help="ask a question that may call local tools")
p_tool.add_argument("question")
sub.add_parser("rate", help="show which price tier applies right now")
args = parser.parse_args()
if args.cmd == "rate":
print("peak" if is_peak() else "off-peak")
return
settings = load_settings()
client = make_client(settings)
ledger = Ledger()
if args.cmd == "chat":
convo = Conversation(client, settings, "You are a concise technical assistant.", ledger)
print("Type 'exit' to quit.")
while True:
try:
text = input("you> ").strip()
except (EOFError, KeyboardInterrupt):
break
if text.lower() in {"exit", "quit"}:
break
if text:
print("ai> ", end="")
convo.ask(text)
elif args.cmd == "extract":
review = extract_review(client, settings, ledger, args.note)
print(review.model_dump_json(indent=2))
elif args.cmd == "tools":
print(chat_with_tools(client, settings, ledger, args.question))
print(f"[{ledger.summary()}]")
if __name__ == "__main__":
main()
The rate command doubles as a scheduling helper. A shell script that runs a batch job can call it and sleep until the output reads off-peak, which is the cheapest optimization in this entire tutorial. It runs before load_settings(), so it works even without a key configured.
Step 11: Write Tests That Cost Nothing
Tests for an API client should never spend money. The pricing module is pure logic, so pytest covers it directly with fixed timestamps: a Wednesday at 07:00 UTC for peak, a Wednesday at 12:00 UTC for off-peak, a Saturday for the weekend rule, and the exact boundary at 03:59 and 04:00. The ledger tests use a plain namespace object in place of a real usage response, once with cache fields and once without.
from datetime import datetime, timezone
from types import SimpleNamespace
from dslab.pricing import Ledger, cost_usd, is_peak
WED_PEAK = datetime(2026, 10, 21, 7, 0, tzinfo=timezone.utc)
WED_OFF = datetime(2026, 10, 21, 12, 0, tzinfo=timezone.utc)
SAT = datetime(2026, 10, 24, 7, 0, tzinfo=timezone.utc)
def test_peak_windows():
assert is_peak(WED_PEAK)
assert not is_peak(WED_OFF)
assert not is_peak(SAT)
assert is_peak(datetime(2026, 10, 21, 3, 59, tzinfo=timezone.utc))
assert not is_peak(datetime(2026, 10, 21, 4, 0, tzinfo=timezone.utc))
def test_cost_matches_rate_card():
assert round(cost_usd(0, 1_000_000, 0, WED_OFF), 6) == 0.15
assert round(cost_usd(0, 1_000_000, 0, WED_PEAK), 6) == 0.30
assert round(cost_usd(0, 0, 1_000_000, WED_OFF), 6) == 0.60
assert round(cost_usd(0, 0, 1_000_000, WED_PEAK), 6) == 1.20
assert round(cost_usd(1_000_000, 0, 0, WED_OFF), 6) == 0.003
def test_ledger_falls_back_when_cache_fields_missing():
usage = SimpleNamespace(prompt_tokens=1500, completion_tokens=400)
ledger = Ledger()
ledger.add(usage, WED_OFF)
assert ledger.miss == 1500 and ledger.hit == 0
assert round(ledger.usd, 6) == 0.000465
def test_ledger_uses_cache_fields_when_present():
usage = SimpleNamespace(
prompt_tokens=1500,
completion_tokens=400,
prompt_cache_hit_tokens=1000,
prompt_cache_miss_tokens=500,
)
ledger = Ledger()
ledger.add(usage, WED_OFF)
assert (ledger.hit, ledger.miss) == (1000, 500)
def test_search_budget_blocks_overspend():
import pytest
from dslab.budget import BudgetExceeded, SearchBudget
budget = SearchBudget(max_usd=0.0015)
budget.charge()
budget.charge()
with pytest.raises(BudgetExceeded):
budget.charge()
assert budget.calls == 2
For end-to-end checks without a key, the project was exercised against a 40-line local mock server that speaks the chat-completions format. Point DEEPSEEK_BASE_URL at it and the whole CLI runs offline, including streaming, JSON mode, and the tool loop. That trick also gives you a safe way to test failure handling: make the mock return a 429 or a malformed JSON string and watch the retry and repair paths fire.
Step 12: Run the Project End to End
With the files in place, run the tests first, then the offline commands, then one live call per feature. Run the live calls off-peak if you can, though at these prices the difference over a few requests rounds to nothing. The expected output for each command appears in the project walkthrough below.
python -m pytest -q
python -m dslab.cli rate
python -m dslab.cli extract "Rotating API keys by hand every quarter, no alert if one leaks."
python -m dslab.cli tools "What would 1,500 input tokens and 400 output tokens cost right now?"
python -m dslab.cli chat
If all five run cleanly, the foundation is finished: configuration, retries, streaming, memory, validated JSON, tool calling, cost tracking, and tests. Everything after this point is hardening and extension.
Search and Fetch APIs: What Changed on October 17 #
DeepSeek’s October 17 release did more than swap a checkpoint. It also opened two retrieval services to outside developers and all users. The Search API returns up to 50 ranked web results per query, and the Fetch API converts a single web page into Markdown. Each is priced at $0.75 per 1,000 successful requests, which works out to $0.00075 per call. Per-request billing starts at 00:00 Beijing time on November 1, 2026 (that is 16:00 UTC on October 31), and until then, according to DeepSeek’s documentation, calls made through DeepSeek Harness, Claude Code, or Codex are free.
That pricing matters because retrieval is where agent bills quietly balloon. A research agent that issues 8 searches and fetches 12 pages per task makes 20 billable calls, or $0.015 per task before a single model token. At 1,000 tasks a day, the retrieval line alone costs $15 a day, and a loop bug that retries forever turns that into a four-figure surprise overnight. The request and response schema for both services lives in the Search guide of DeepSeek’s API docs, which is the authority to follow when you wire them in. The tutorial project does not hard-code a guessed schema.
What the project does provide is the guardrail that belongs in front of any per-call tool. SearchBudget counts calls, converts them into dollars at the published rate, and raises an exception the moment the next call would pass a cap you set. Call budget.charge() before each Search or Fetch request and a runaway agent stops at a number you chose.
class BudgetExceeded(RuntimeError):
pass
class SearchBudget:
"""Hard spending cap for per-request tools such as Search and Fetch."""
USD_PER_CALL = 0.75 / 1000
def __init__(self, max_usd: float):
self.max_usd = max_usd
self.calls = 0
@property
def spent(self) -> float:
return self.calls * self.USD_PER_CALL
def charge(self) -> None:
if (self.calls + 1) * self.USD_PER_CALL > self.max_usd:
raise BudgetExceeded(f"cap of ${self.max_usd:.2f} reached after {self.calls} calls")
self.calls += 1
A final caution on third-party tooling. Several tracker sites already publish unofficial summaries of the Search and Fetch endpoints. Some of them are accurate, and some copy each other’s mistakes. Before you build on a request shape, confirm it against the official docs and test it with a handful of paid calls, because the rate of $0.75 per 1,000 leaves plenty of room for that experiment.
The Complete Project: Layout, Run Commands and Expected Output #
The finished project has one package, one test file, and a few small support files. Every file appeared in full in the steps above, so the tree below doubles as a checklist. Related model families and their own API setups are collected in the AI and machine learning section for readers who want to compare providers.
deepseek-lab/
├── .env # secrets, ignored by Git
├── .gitignore
├── requirements.txt
├── hello.py # Step 4 smoke test
├── dslab/
│ ├── __init__.py
│ ├── config.py # Step 3
│ ├── client.py # Step 3
│ ├── chat.py # Steps 5-6
│ ├── structured.py # Step 7
│ ├── tools.py # Step 8
│ ├── pricing.py # Step 9
│ ├── cli.py # Step 10
│ ├── budget.py # Search and Fetch spending cap
│ └── fanout.py # concurrent batch runner (Advanced Tips)
└── tests/
└── test_pricing.py # Step 11
Illustrative output follows. Token counts, wording, and cost will differ on the live API, and only the test line is a literal capture. The shape is what matters: each command prints its result, then one ledger line.
$ python -m pytest -q
..... [100%]
5 passed in 0.03s
$ python -m dslab.cli rate
off-peak
$ python -m dslab.cli extract "Rotating API keys by hand every quarter, no alert if one leaks."
{
"summary": "Manual quarterly key rotation with no leak alerting.",
"risks": [
"Leaked keys stay valid until the next rotation",
"No detection of unauthorized usage"
],
"severity": "high"
}
[1 call(s) | in 120 tok (83% cached) | out 30 tok | est. $0.000021]
$ python -m dslab.cli tools "What would 1,500 input tokens and 400 output tokens cost right now?"
About $0.000465 at the off-peak rate.
[2 call(s) | in 240 tok (83% cached) | out 60 tok | est. $0.000043]
$ python -m dslab.cli chat
Type 'exit' to quit.
you> Explain context caching in two sentences.
ai> Context caching stores the processed form of a repeated prompt prefix so later requests skip that work.
Cached input tokens are billed at a small fraction of the normal input price.
you> exit
[1 call(s) | in 64 tok (0% cached) | out 38 tok | est. $0.000032]
Read the ledger line as a feedback loop. A first call shows 0% cached. Repeat the same system prompt and a long shared prefix a few times and the percentage climbs, and the estimated cost per call drops with it. That visible drop is the cheapest education in prompt design you will get.
Common Pitfalls With the DeepSeek API #
Most DeepSeek API failures come from a short list of avoidable habits. Each one below has cost someone real money or an afternoon of debugging.
- Pinning a version suffix or a retired alias. Code that sends
DeepSeek-V4.1-Flash-1017ordeepseek-v4-proas the model name depends on names that DeepSeek’s own notices describe as a checkpoint label and a routed alias. Usedeepseek-flashfrom configuration, and log the model the server reports so you can see changes. - Budgeting with one flat price. Peak output costs $1.20 per million tokens against $0.60 off-peak. A cost model without the time-of-day split can be off by a factor of two, and a factor of two is the difference between an approved and a rejected budget.
- Ignoring the cache. At $0.003 against $0.15 per million tokens off-peak, a cache hit is far cheaper than a miss. Putting the variable user text first and the stable instructions last wipes out that discount. Stable prefix first, variable tail last.
- Streaming without usage reporting. Leave out
stream_optionsand the ledger reads zero for every streamed reply. Teams discover this at invoice time. - Trusting JSON mode as a schema guarantee. It promises parseable JSON in the best case. Field names, types, and enum values still need validation, which is why Step 7 uses Pydantic and a single repair pass.
- Letting the tool loop run free. A model that keeps requesting tools drains money and time. Cap the rounds, cap the per-run spend, and fail loudly when the cap trips.
- Stacking retry layers. The SDK retries, your wrapper retries, and your job queue retries. Three layers of three attempts make nine calls for one failure and hammer a provider that is already struggling. Pick one owner for retry policy.
- Committing the key. Automated scanners watch public repositories for leaked secrets. If a key lands in Git history, revoke it immediately, because deleting the file does not remove the history.
- Skipping timeouts. A hung connection with no timeout holds a worker forever. The project sets a 60-second default, and long generations should raise it deliberately rather than remove it.
A quiet tenth pitfall deserves a mention: treating a model’s confident answer as verified fact. Validation catches malformed output and does nothing for a wrong answer in the right format. For anything that touches money, health, or legal text, keep a human or a deterministic check in the loop.
Troubleshooting: 10 Errors and Fixes #
Each entry lists the symptom, the likely cause, and the fix. The HTTP status codes below follow standard conventions. DeepSeek’s docs carry the authoritative error table, so check it when a code behaves unexpectedly.
- 401 Unauthorized or authentication failed. The key is missing, mistyped, revoked, or pasted with a trailing space or newline. Print
len(settings.api_key)to check for stray characters, regenerate the key if needed, and confirm the shell session actually exports the variable. - 402 or an insufficient-balance error. The prepaid balance reached zero. Top up in the console. No retry will fix it, which is why the retry list excludes this error.
- 404 or model not found. The model string is wrong or retired. Reset
DEEPSEEK_MODELtodeepseek-flash. Names from September-era tutorials, includingdeepseek-v4-pro, can behave differently now that DeepSeek routes that name to Flash. - 429 Too Many Requests. You hit a rate limit. Lower concurrency in
fanout.py(theSemaphorevalue), keep the jittered backoff, and spread batch work over time. If you run the model through Cloudflare Workers AI instead, remember its separate default limits. - Server errors or long hangs. Provider-side trouble. Secondary reports described an outage lasting about five hours before the 1017 release, so plan for it: keep timeouts short, let the retry policy absorb brief blips, and add a fallback provider for critical paths.
JSONDecodeErroror a PydanticValidationErrorin JSON mode. The reply is wrapped in a code fence, truncated by a lowmax_tokens, or missing a field. The repair loop handles the first case. For truncation, raisemax_tokens. For missing fields, tighten the schema description in the system prompt.- Ledger shows zero tokens on streamed calls.
stream_options={'include_usage': True}is absent, or a proxy strips the final usage chunk. Call the endpoint directly with the option set and compare. Some gateways need explicit configuration to forward that chunk. - Tool call arguments fail to parse. Models occasionally emit malformed argument strings. The loop returns an error object to the model, which usually self-corrects on the next round. If it keeps failing, simplify the schema, mark fewer fields as required, and add a one-line example to the tool description.
ModuleNotFoundError: No module named 'dslab'. You ran a script from the wrong folder or without-m. Run commands from the project root withpython -m dslab.cli, and confirm the empty__init__.pyexists.- SSL errors, proxy errors, or connection resets. Corporate proxies and outdated certificate bundles cause these. Upgrade
certifiwith pip, setHTTPS_PROXYif your network requires it, and test with a barecurlto the base URL to separate network problems from code problems.
When none of those match, log the full request ID and status code from the exception, then reproduce with the smallest possible call: one user message, no tools, no streaming. Add features back one at a time until the failure returns. That bisection finds the culprit faster than any amount of staring at a stack trace.
Advanced Tips for Cost, Speed and Reach #
Shape Prompts for Cache Hits
Put everything stable at the top: system rules, style guides, few-shot examples, reference documents. Put everything that varies at the bottom: the user’s question, the retrieved snippets. Keep the stable block byte-identical between calls, because a single changed character early in the prompt can invalidate the cached prefix. Log the hit ratio the ledger prints and treat anything under 50% on a repetitive workload as a bug to chase.
Run Batch Work Off-Peak and in Parallel
Peak pricing doubles every token, so batch jobs belong in the off-peak windows: weekends, 04:00 to 06:00 UTC, and after 10:00 UTC on weekdays. The asynchronous runner below fans prompts out with a semaphore that caps concurrency at 8, which keeps you under rate limits while still finishing a 1,000-prompt job in a fraction of the serial time. Rate cards also change, so keep them in one module where a price update is a one-line edit and a failing test reminds you to review it.
import asyncio
from openai import AsyncOpenAI
from .config import load_settings
from .pricing import Ledger
async def run_batch(prompts: list[str], concurrency: int = 8) -> tuple[list[str], Ledger]:
settings = load_settings()
client = AsyncOpenAI(
api_key=settings.api_key,
base_url=settings.base_url,
timeout=settings.timeout,
max_retries=3,
)
ledger = Ledger()
gate = asyncio.Semaphore(concurrency)
async def one(prompt: str) -> str:
async with gate:
resp = await client.chat.completions.create(
model=settings.model,
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
ledger.add(resp.usage)
return resp.choices[0].message.content or ""
try:
answers = await asyncio.gather(*(one(p) for p in prompts))
finally:
await client.close()
return list(answers), ledger
if __name__ == "__main__":
out, led = asyncio.run(run_batch([f"Give one fact about the number {n}." for n in range(1, 6)]))
print(*out, sep="\n")
print(led.summary())
Reach the Same Model Through Cloudflare Workers AI
Cloudflare’s changelog says DeepSeek V4.1 Flash 1017 became available on Workers AI on October 19, and it lists the model under a @cf/deepseek-ai identifier. According to Cloudflare’s changelog entry, using it requires a Workers Paid plan or prepaid AI Gateway credits, with default limits reported at 20 requests per minute on standard Workers AI billing and 50 requests per minute with prepaid AI Gateway credits. A second route gives you a fallback when the primary endpoint struggles. The cost is a different SDK call shape, different limits, and a different bill, so wrap both behind one function and test the failover path before an outage forces it.
Self-hosting is the other route. DeepSeek reportedly open-sourced DeepInfer, the inference framework behind its production services, on October 20 (details in our coverage of the DeepInfer release). Running your own inference takes GPU capacity and operations work, so it suits teams with steady volume or strict data rules and rarely suits a weekend prototype.
Two more habits pay off in production. First, log the token counts and the ledger estimate for every request with a request ID, so a spike traces back to one caller in minutes. Second, add a canary: a scheduled job that sends one tiny, fixed prompt every few minutes and alerts on errors or latency jumps. Providers rarely announce brownouts, and a canary tells you before your users do.
Security and Compliance Checks Before Production #
Key hygiene comes first. Rotate keys on a schedule and immediately after any suspected exposure, scope one key to one service, and set spending alerts. Keep keys out of client-side code entirely: a mobile app or browser bundle that carries a provider key hands it to every user who opens a network inspector. Route client traffic through your own backend, which holds the key, authenticates the user, and rate-limits per account.
Policy risk is the second check. On October 20, 2026, the US administration barred DeepSeek from civilian federal agency systems, CNBC reported. Our breakdown of the DHS ban under the 2018 law covers the legal mechanics. If you build for government customers, contractors, or regulated industries, ask the customer’s compliance team about DeepSeek before you design around it, because a policy change can make a working integration unusable without any change in the code.
Data handling is the third check. Anything you send to a hosted API leaves your network. Strip personal data, secrets, and customer identifiers from prompts unless a contract and a data-processing review say otherwise. Log prompts carefully, since logs become a second copy of that data. Reports say DeepSeek released weights alongside the 1017 update, and a self-hosted deployment is one path for teams that cannot send data out. Read the model card for license terms and hardware needs before you commit to that route.
Prompt injection is the fourth. Any text a tool fetches from the web or a user uploads can carry instructions aimed at your model. Treat tool output as untrusted data, never concatenate it into the system prompt, restrict which tools a retrieval-driven agent can call, and require confirmation for any action with side effects. The Search and Fetch APIs make this concrete, because they pipe arbitrary web content straight into the model’s context.
Finally, log what matters and nothing more. Model name, token counts, latency, status code, and request ID belong in the log. Full prompts and completions belong there only when you have a reason and a retention rule.
FAQ #
What model name should I use for the DeepSeek API in October 2026?
Use deepseek-flash. It points at the latest V4.1 Flash build, currently V4.1-Flash-1017, released October 17, 2026. DeepSeek’s changelog says to use that alias, and the API calling method did not change with the update.
Is the DeepSeek API compatible with the OpenAI Python SDK?
Earlier DeepSeek releases accepted the OpenAI chat-completions format, and this project relies on that by changing only the base URL and key. Confirm the current details on DeepSeek’s API docs page titled Your First API Call, since this guidance depends on that format staying stable.
How much does the DeepSeek API cost?
For deepseek-flash, off-peak rates are $0.003 per million cache-hit input tokens, $0.15 per million cache-miss input tokens, and $0.60 per million output tokens. Peak rates are double: $0.006, $0.30, and $1.20. Search and Fetch calls cost $0.75 per 1,000 successful requests, billed from November 1, 2026.
When is the DeepSeek API at peak pricing?
Peak applies Monday through Friday from 01:00 to 04:00 UTC and from 06:00 to 10:00 UTC, excluding Chinese public holidays, based on DeepSeek’s pricing notice. Everything else counts as off-peak at half the peak rate.
Does deepseek-v4-pro still work?
Since September 14, 2026, requests using deepseek-v4-pro route to V4.1 Flash and bill at Flash rates, a routing DeepSeek said would last until V4.1-Pro launches. Switch your configuration to deepseek-flash so the name matches what you actually get.
Are the Search and Fetch APIs free?
Not permanently. Per-request billing at $0.75 per 1,000 successful requests begins at 00:00 Beijing time on November 1, 2026. Until then, according to DeepSeek’s documentation, calls made through DeepSeek Harness, Claude Code, or Codex are free. Put a spending cap in front of both before the date arrives.
How do I cut my DeepSeek API bill the most?
Three levers matter most. Put stable instructions at the front of every prompt to raise cache hits. Run batch jobs off-peak. Cap tool rounds and retrieval calls so an agent bug cannot loop. The ledger in this project shows the effect of each change in dollars.
Can US government contractors use the DeepSeek API?
That depends on contract terms and agency policy. The US administration barred DeepSeek from civilian federal agency systems on October 20, 2026, according to CNBC. Ask your contracting officer or compliance team before building on the service for any federal work.