DeepSeek V4.1-Flash-1017 in Python: Tested Code for Tools, JSON and Reasoning
DeepSeek V4.1-Flash-1017 Python tutorial: call deepseek/deepseek-v4.1-flash through SandBase with requests, stream, run tools, get JSON Schema output, control reasoning and handle errors. Every snippet run live.

My first useful lesson with DeepSeek V4.1-Flash-1017 was an empty answer. I sent a hard counting question with max_tokens=300, got HTTP 200, finish_reason: "length", and content: "". The usage block explained it: all 300 completion tokens were reasoning tokens I never saw, and I was billed for every one. This model reasons before it answers, and that single behavior shapes most of the code below.
This is a DeepSeek V4.1-Flash-1017 Python tutorial for people who want working code, not a feature list. It calls deepseek/deepseek-v4.1-flash — the id that now serves the October 1017 checkpoint — through SandBase’s OpenAI-compatible Chat Completions endpoint, using plain requests. Every snippet ran against the live model on 2026-10-17, and every cost comes from GET /v1/tasks/<id>/cost, not from my arithmetic.
Key takeaway
- Point any OpenAI-compatible client at
https://api.sandbase.ai/v1and use the model iddeepseek/deepseek-v4.1-flash. Streaming, parallel tool calls, andresponse_formatJSON Schema all worked on the first try.- Hidden reasoning tokens share the
max_tokensbudget and bill at the output rate. At 300 tokens the model returned an empty answer and was billed for 300 output tokens; at 16,000 it answered correctly in far fewer.reasoning_effortchanges behavior: on one hard prompt, median reasoning tokens were 1,678 (low), 1,853 (high), and 2,737 (unset) over 3 runs each, all correct. An invalid value returned HTTP 503, so validate before you send.- A request with no
messagescame back as HTTP 503, not 400 — a naive “retry every 5xx” loop retries a client bug. Check the error before retrying.- The whole test pass, 15 billed calls, cost $0.0176 at an effective rate near $0.22 / $0.90 per million tokens (a 25% launch discount on the $0.30 / $1.20 list price at capture time).
Before you start
You need Python 3.10+, a SandBase API key in SANDBASE_API_KEY, and the requests package. The snippets use raw HTTP so the mechanics are visible; the openai SDK works too, which I cover at the end.
python3 -m venv .venv && source .venv/bin/activate
pip install "requests==2.32.5"
export SANDBASE_API_KEY="your-key"
Two facts decide how you write the code. First, limits: the SandBase model page lists a 1M context window and a 384K max output, so max_tokens has real headroom. Second, price: the model lists at $0.30 input and $1.20 output per million tokens, and at capture time a 25% launch discount billed it near $0.22 / $0.90. Reasoning tokens are billed as output.
The model page lists the exact id used in every snippet, the discounted token price behind the costs below, and the 384K max output that bounds max_tokens (captured 2026-10-17).
Step 1: one call with requests
This is the shape every other step builds on.
import os
import requests
BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
resp = requests.post(
f"{BASE}/chat/completions",
headers=HEADERS,
json={
"model": "deepseek/deepseek-v4.1-flash",
"messages": [
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "What does HTTP status 429 mean for an API client?"},
],
"max_tokens": 1024, # covers hidden reasoning tokens AND the visible answer
},
timeout=120,
)
resp.raise_for_status()
body = resp.json()
choice = body["choices"][0]
print("id:", body["id"])
print("finish_reason:", choice["finish_reason"])
print("answer:", choice["message"]["content"])
print("usage:", body["usage"])
cost = requests.get(f"{BASE}/tasks/{body['id']}/cost", headers=HEADERS, timeout=30).json()
print("billed:", cost["cost"], cost["currency"])
Live output, trimmed to the fields that matter:
id: b552c90b-4e94-4e96-b1cb-62bee2f39485
finish_reason: stop
answer: HTTP 429 means the API client has sent too many requests in a given amount of
time and is being rate-limited, so it should back off and retry later ...
usage: {'completion_tokens': 82, 'completion_tokens_details': {'reasoning_tokens': 36},
'prompt_tokens': 47, 'total_tokens': 129}
billed: 0.000084 USD
Note the 36 reasoning tokens on a one-sentence answer. The /cost response also showed the discount applied: an original_cost of $0.000113 with a 25% model discount, settling at $0.000084. Keep the id; it is your only handle for reconciling cost later.
Step 2: streaming, with usage at the end
Set stream: true, read Server-Sent Events, and ask for a final usage chunk with stream_options.
import os, json, time, requests
BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
start = time.monotonic(); ttft = None; parts = []; usage = None; cid = None
with requests.post(f"{BASE}/chat/completions", headers=HEADERS, stream=True, timeout=180, json={
"model": "deepseek/deepseek-v4.1-flash",
"messages": [{"role": "user", "content": "Write a 4-line poem about rate limits."}],
"max_tokens": 2048, "stream": True, "stream_options": {"include_usage": True},
}) as s:
for line in s.iter_lines():
if not line or not line.decode().startswith("data: "):
continue
payload = line.decode()[6:]
if payload == "[DONE]":
break
chunk = json.loads(payload)
cid = cid or chunk.get("id")
if chunk.get("usage"):
usage = chunk["usage"]
for ch in chunk.get("choices", []):
piece = ch.get("delta", {}).get("content")
if piece:
if ttft is None:
ttft = time.monotonic() - start
parts.append(piece)
task_id = cid.removeprefix("chatcmpl-") # streamed ids carry a chatcmpl- prefix
print("first token:", round(ttft, 1), "s | total:", round(time.monotonic() - start, 1), "s")
print("usage:", usage)
print(requests.get(f"{BASE}/tasks/{task_id}/cost", headers=HEADERS, timeout=30).json()["cost"])
On my run the first visible token arrived after 4.9 s and the stream finished at 5.0 s, with 40 prompt and 209 completion tokens — 173 of them reasoning. That slow first token is the model thinking before it writes; it is one run from my network, not a latency benchmark. Billed: $0.000197.
One trap worth knowing: streamed chunks carry an id like chatcmpl-e3857df2-..., but GET /v1/tasks/chatcmpl-.../cost returns 404. Strip the prefix first — task_id = cid.removeprefix("chatcmpl-") — and the cost lookup works.
SandBase’s Chat Completions guide documents the base URL, the stream_options.include_usage flag used here, and the tool and structured-output features used in Steps 3 and 4 (captured 2026-10-17).
Step 3: function calling, full round trip
Send tools, run whatever the model asks for, append one tool message per call with the matching tool_call_id, and ask again. The tool is a local dictionary so the result is checkable.
import os, json, requests
BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
MODEL = "deepseek/deepseek-v4.1-flash"
ORDERS = {"A-1001": {"status": "shipped", "carrier": "DHL", "eta": "2026-10-19"},
"A-1002": {"status": "processing", "carrier": None, "eta": None}}
def get_order_status(order_id: str) -> dict:
return ORDERS.get(order_id, {"error": f"unknown order {order_id}"})
TOOLS = [{"type": "function", "function": {
"name": "get_order_status",
"description": "Look up shipping status for one order id.",
"parameters": {"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]}}}]
messages = [{"role": "user", "content": "Where are orders A-1001 and A-1002? One line each."}]
for turn in range(5): # hard cap so a confused model can't loop forever
r = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=120,
json={"model": MODEL, "messages": messages, "tools": TOOLS, "max_tokens": 2048})
msg = r.json()["choices"][0]["message"]
calls = msg.get("tool_calls") or []
print(f"turn {turn}: tool_calls={len(calls)}")
if not calls:
print("answer:", msg["content"]); break
messages.append({k: v for k, v in msg.items() if v is not None})
for call in calls:
args = json.loads(call["function"]["arguments"] or "{}")
result = get_order_status(**args)
messages.append({"role": "tool", "tool_call_id": call["id"], "content": json.dumps(result)})
else:
raise RuntimeError("tool loop did not finish in 5 turns")
On turn 0 the model asked for both orders in a single response (two parallel tool_calls, 102 completion tokens, $0.000162). On turn 1 it answered without inventing anything for the order that has no carrier:
- A-1001: shipped via DHL, ETA 2026-10-19.
- A-1002: still processing — no carrier or ETA yet.
Turn 1 billed only $0.000079, and the usage block showed why: 256 of its prompt tokens were a cache hit. The conversation prefix was reused, so the second turn was cheaper than its token count suggests. In a longer agent, that prompt-cache behavior is the difference between a linear and a quadratic bill.
Step 4: structured output with response_format
import os, json, requests
BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
SCHEMA = {
"type": "object",
"properties": {
"vendor": {"type": "string"},
"invoice_number": {"type": "string"},
"total_usd": {"type": "number"},
"line_items": {"type": "array", "items": {
"type": "object",
"properties": {"description": {"type": "string"}, "amount_usd": {"type": "number"}},
"required": ["description", "amount_usd"], "additionalProperties": False}},
},
"required": ["vendor", "invoice_number", "total_usd", "line_items"],
"additionalProperties": False,
}
TEXT = ("Invoice INV-2291 from Northwind Hosting. Dedicated server October: $1,200.00. "
"Backup storage 2 TB: $80.00. Support plan: $150.00. Amount due: $1,430.00.")
r = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=120, json={
"model": "deepseek/deepseek-v4.1-flash",
"messages": [{"role": "user", "content": f"Extract the invoice fields.\n\n{TEXT}"}],
"response_format": {"type": "json_schema",
"json_schema": {"name": "invoice", "strict": True, "schema": SCHEMA}},
"max_tokens": 2048})
choice = r.json()["choices"][0]
if choice["finish_reason"] != "stop":
raise RuntimeError(f"incomplete JSON risk: finish_reason={choice['finish_reason']}")
data = json.loads(choice["message"]["content"]) # raises if the model returned non-JSON
items_sum = round(sum(i["amount_usd"] for i in data["line_items"]), 2)
print(json.dumps(data, indent=2))
print("line items sum:", items_sum, "| matches total:", items_sum == data["total_usd"])
The model returned bare JSON with all three line items, total_usd: 1430.0, and the sum check passed. It used 157 completion tokens (78 reasoning) for 228 prompt tokens, billed $0.000193. Even in strict JSON mode, keep the finish_reason check and the required-key parse — a truncated response is still invalid JSON, and that failure mode doesn’t go away because you asked for a schema.
Step 5: the reasoning budget, and max_tokens
Back to the empty answer. This model reasons before it answers, those reasoning tokens count inside completion_tokens, and max_tokens caps both together. A cap that looks generous for a one-word answer can be spent entirely on thinking. The hard prompt below is “How many 7-digit positive integers have a digit sum of exactly 30?” — the correct answer, brute-forced locally, is 453,438.
import os, requests
BASE = "https://api.sandbase.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}"}
PROMPT = "How many 7-digit positive integers have a digit sum of exactly 30? Reply with the number only."
def ask(prompt, max_tokens, retry_budget=16000):
r = requests.post(f"{BASE}/chat/completions", headers=HEADERS, timeout=180,
json={"model": "deepseek/deepseek-v4.1-flash",
"messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens})
c = r.json()["choices"][0]; u = r.json()["usage"]
print(f"max_tokens={max_tokens}: finish={c['finish_reason']} content={c['message']['content']!r} "
f"reasoning={u['completion_tokens_details']['reasoning_tokens']}")
if c["finish_reason"] == "length":
if max_tokens >= retry_budget:
raise RuntimeError("still truncated at the retry budget; shorten the task")
return ask(prompt, retry_budget, retry_budget) # reasoning shares this budget
return c["message"]["content"]
print("final:", ask(PROMPT, max_tokens=300))
At max_tokens=300 the call stopped with finish_reason: length, empty content, and 300 reasoning tokens — billed $0.000282 for an answer I never got. The retry at 16,000 returned 453438. Three rules follow: check finish_reason on every response, treat empty content plus length as “budget too small” rather than “model refused”, and don’t set max_tokens tight to save money on a reasoning model — set it to fit and control cost with the prompt.
The companion control is reasoning_effort. On this hard prompt, three runs per setting:
reasoning_effort | Reasoning tokens per run | Median | Correct |
|---|---|---|---|
| unset | 2,206 / 2,737 / 2,866 | 2,737 | 3/3 |
"low" | 1,678 / 1,306 / 1,914 | 1,678 | 3/3 |
"high" | 1,256 / 2,450 / 1,853 | 1,853 | 3/3 |
Every setting got the right answer, and low used the fewest reasoning tokens on median — a real lever for cost on easy-to-medium tasks. The ranges overlap and N is 3, so treat this as a direction, not a guarantee; measure on your own prompts. One more finding: sending reasoning_effort: "banana" returned HTTP 503, not a clean validation error, so stick to low / medium / high and don’t assume an invalid value fails safely. Those nine hard-prompt calls were the biggest line in the test, $0.0166 together.
Step 6: error handling and retries
I sent the usual mistakes with raw requests to see the real status codes:
| Mistake | HTTP | Body (trimmed) |
|---|---|---|
| Wrong API key | 401 | {"error":"invalid API key"} |
deepseek/deepseek-v4.1-flash-preview | 404 | type: not_found_error, “model not found” |
No messages field | 503 | type: server_error, “Input required: specify … messages” |
Two things to design around. The 401 body is a plain string under error, while the others are OpenAI-style objects, so read errors defensively. And the missing-messages bug returns a 503, which most generic retry policies treat as transient — so a naive “retry all 5xx” loop will retry a request that can never succeed. Validate messages locally, retry only 429/500/502/503/504, and never auto-retry a tool turn whose side effects may already have run.
Using the openai SDK instead
Everything above works through the openai SDK with two changes — the base URL and the model id — because SandBase is OpenAI-compatible. I didn’t re-run every step through the SDK, so I won’t quote SDK-specific outputs, but the client setup is documented:
from openai import OpenAI
client = OpenAI(base_url="https://api.sandbase.ai/v1", api_key=os.environ["SANDBASE_API_KEY"])
resp = client.chat.completions.create(
model="deepseek/deepseek-v4.1-flash",
messages=[{"role": "user", "content": "Hello!"}], max_tokens=1024)
DeepSeek’s change log confirms deepseek-flash now serves the 1017 checkpoint upstream; on SandBase the equivalent id is deepseek/deepseek-v4.1-flash (captured 2026-10-17).
What the whole tutorial cost
| Step | Billed calls | Cost |
|---|---|---|
| Steps 1–4 (basic, streaming, tools, JSON) | 5 | $0.0007 |
| Step 5 reasoning-effort probe (9 calls) | 9 | $0.0166 |
| Step 5 max_tokens truncation | 1 | $0.0003 |
| Total | 15 | $0.0176 |
Failed requests (401, 404, 503) returned no completion id, so there was nothing to bill. Every row is the sum of /v1/tasks/<id>/cost responses at the discounted rate in effect on 2026-10-17.
FAQ
Which model id do I use for DeepSeek V4.1-Flash-1017 on SandBase?
deepseek/deepseek-v4.1-flash. DeepSeek upgraded this in place to the 1017 checkpoint, so there is no -1017 suffix in the API id. Point your client at https://api.sandbase.ai/v1.
Why is my DeepSeek response empty?
Check finish_reason. If it is length, hidden reasoning tokens used up max_tokens before the visible answer started. Raise max_tokens; reasoning tokens bill as output either way.
Does reasoning_effort reduce cost?
On one hard prompt, low used fewer reasoning tokens on median than unset or high, over three runs each, and still answered correctly. The ranges overlapped, so measure it on your own workload rather than assuming a fixed saving.
Can I use the openai Python SDK?
Yes. Set base_url="https://api.sandbase.ai/v1", pass your SandBase key, and use model="deepseek/deepseek-v4.1-flash". The raw-requests snippets above map directly onto the SDK’s client.chat.completions.create call.
Limits of this tutorial and next steps
Everything here is one run per snippet (three per setting for reasoning_effort), from one network, on one day. Latency figures are anecdotes, not benchmarks. I didn’t test image input, prompt-cache controls, or the Anthropic-format endpoint. For what changed in this checkpoint, see the 1017 update breakdown; for how it compares with V4 Pro on price and vision, see V4.1 Flash vs V4 Pro; and for the search tools DeepSeek shipped the same day, see the Search and Fetch API guide. To run this code yourself, grab a key and the model id from the DeepSeek V4.1 Flash model page and start with Step 5’s ask() — the reasoning-budget guard is the piece I’d keep in any production client.