If you're used to a chat_template.jinja file, you know the idea: turn a list of chat messages into prompt text before tokenization.
DeepSeek Flash 1017 doesn't ship one. Instead there's a 38 KB Python file called encoding.py [1]. 🕵️
That sounds annoying. It's actually a gift. You can run that file on your laptop, with no model, no GPU and no download of the weights, and inspect the prompt text before tokenization or inference. Let's do that.
Original meme. Emoji: Twemoji, CC-BY 4.0.
Grab the encoder, skip the weights
All you need is Python 3. The parts we used only need the standard library. We ran it on Python 3.14.7.
Make a folder and fetch the file, pinned to the exact version we tested so nothing shifts under you:
mkdir deepseek-format-demo cd deepseek-format-demo curl --fail --location --output encoding.py https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017/resolve/1bae0e1088993b9b82de08f59de5be5a34e42170/encoding/encoding.py curl --fail --location --output LICENSE https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017/resolve/1bae0e1088993b9b82de08f59de5be5a34e42170/LICENSE Check you got the same file (37,962 bytes):
python3 -c "import hashlib; print(hashlib.sha256(open('encoding.py','rb').read()).hexdigest())" # 13231c592e958fb39e2af6001dae69d90e7a36b265702b9b390236fb387599e2 It's MIT licensed. Keep the LICENSE next to it if you copy it into a project.
Clue 1: the prompt ends mid-sentence
Save this as demo.py:
from encoding import encode_messages messages = [{'role': 'user', 'content': 'What is 2 + 2?'}] prompt = encode_messages(messages, thinking_mode='chat') print(prompt) assert prompt.endswith('<|Assistant|></think>') Run python3 demo.py. The output stops at an assistant marker. No 4 anywhere.
That's the point. The encoder builds the question and then leaves the door open: "assistant, your turn". The model writes everything after that marker.
Now flip on thinking mode:
prompt = encode_messages( messages, thinking_mode='thinking', reasoning_effort=75, ) This time the prompt gets a Reasoning Effort: 75 line, and it ends with <|Assistant|><think>. The door now opens into the thinking section. 💡
We also tried reasoning_effort=101. The encoder refused it: the effort has to be a whole number from 1 to 100, or one of the words low, high or max. The check happens before anything reaches a model.
Clue 2: tool results get put back in order
Here's where it gets interesting. Say the model calls two tools at once: a looks up Pune, b looks up Delhi. Real tools don't finish in order. Delhi answers first.
Replace demo.py with:
import json from encoding import encode_messages calls = [ {'id': ident, 'type': 'function', 'function': { 'name': 'lookup', 'arguments': json.dumps({'city': city}), }} for ident, city in [('a', 'Pune'), ('b', 'Delhi')] ] messages = [ {'role': 'user', 'content': 'Look up Pune and Delhi.'}, {'role': 'assistant', 'content': '', 'tool_calls': calls}, {'role': 'tool', 'tool_call_id': 'b', 'content': 'RESULT_DELHI'}, {'role': 'tool', 'tool_call_id': 'a', 'content': 'RESULT_PUNE'}, ] prompt = encode_messages(messages, thinking_mode='chat') print(prompt) assert prompt.index('RESULT_PUNE') < prompt.index('RESULT_DELHI') Run it. The assert passes. Pune comes first in the prompt, even though Delhi arrived first. ✅
Diagram from our local check. The tool results are fake strings; no weather service or model was called.
The encoder matches each result to its call using tool_call_id, and lays them out in the order the calls were made [2]. The weird RESULT_PUNE strings are just there so you can spot them in the printout.
Original meme. Emoji: Twemoji, CC-BY 4.0.
⚠️ Common mistake: writing your own adapter and dropping
tool_call_idalong the way. That can break result matching or make encoding fail. Keep the ID from the moment the tool is called until the result goes back in.
Clue 3: reading the model's answer back
The same file can also go the other way: take the text a model wrote and pull out the tool call. We fed parse_message_from_completion_text a hand-written, well-formed tool call. It returned the function name lookup with the argument city: Bengaluru.
Two honest notes. That completion was written by us, not by DeepSeek. And the docs warn the parser expects well-formed output [2]. So in a real app you still have to check the tool name is one you allow, the arguments make sense, and the output isn't cut off halfway.
⚠️ Common mistake: treating "the parser returned a dictionary" as "the tool call is safe to run". Parsing is step one. Deciding whether to run it is your job.
What we checked
Six small checks, all passed:
- Chat mode ends with the assistant marker.
- Thinking mode adds the effort line and opens the thinking section.
- An effort of 101 is rejected.
- Out-of-order tool results are put back in call order.
- A system message in the middle of a chat gets its own marker.
- The parser pulls
lookupandcity: Bengaluruout of a hand-written tool call.
Fine print
This is a walkthrough of the format, not the full upstream test suite. We didn't check token IDs, images or how well the model answers. No model ran at all.
The useful bit: when a DeepSeek integration acts strange, you can now save the conversation, run it through encoding.py, and look at the exact string. That beats guessing whether a borrowed template got it right. 🔍
References
- DeepSeek Flash 1017 model card, prompt encoding section.
- Encoding README at the tested revision.
- The pinned encoding.py.
Asset credits: diagram and memes are original. Emoji graphics: Twemoji, CC-BY 4.0.
AI disclosure
AI was used to draft the article.


