jon1012 commited on
Commit
7a7a5d2
·
verified ·
1 Parent(s): 55d8415

EXL3 weights

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. LICENSE +21 -0
  3. config.json +184 -0
  4. encoding/README.md +305 -0
  5. encoding/__pycache__/encoding.cpython-312.pyc +0 -0
  6. encoding/encoding.py +990 -0
  7. encoding/test_encoding.py +652 -0
  8. encoding/tests/test_input_1.json +82 -0
  9. encoding/tests/test_input_2.json +24 -0
  10. encoding/tests/test_input_3.json +93 -0
  11. encoding/tests/test_input_4.json +28 -0
  12. encoding/tests/test_input_5.json +39 -0
  13. encoding/tests/test_output_1.txt +38 -0
  14. encoding/tests/test_output_2.txt +1 -0
  15. encoding/tests/test_output_3.txt +35 -0
  16. encoding/tests/test_output_4.txt +2 -0
  17. encoding/tests/test_output_5.txt +11 -0
  18. exl3_k_map.json +99 -0
  19. model-00001-of-00044.safetensors +3 -0
  20. model-00002-of-00044.safetensors +3 -0
  21. model-00003-of-00044.safetensors +3 -0
  22. model-00004-of-00044.safetensors +3 -0
  23. model-00005-of-00044.safetensors +3 -0
  24. model-00006-of-00044.safetensors +3 -0
  25. model-00007-of-00044.safetensors +3 -0
  26. model-00008-of-00044.safetensors +3 -0
  27. model-00009-of-00044.safetensors +3 -0
  28. model-00010-of-00044.safetensors +3 -0
  29. model-00011-of-00044.safetensors +3 -0
  30. model-00012-of-00044.safetensors +3 -0
  31. model-00013-of-00044.safetensors +3 -0
  32. model-00014-of-00044.safetensors +3 -0
  33. model-00015-of-00044.safetensors +3 -0
  34. model-00016-of-00044.safetensors +3 -0
  35. model-00017-of-00044.safetensors +3 -0
  36. model-00018-of-00044.safetensors +3 -0
  37. model-00019-of-00044.safetensors +3 -0
  38. model-00020-of-00044.safetensors +3 -0
  39. model-00021-of-00044.safetensors +3 -0
  40. model-00022-of-00044.safetensors +3 -0
  41. model-00023-of-00044.safetensors +3 -0
  42. model-00024-of-00044.safetensors +3 -0
  43. model-00025-of-00044.safetensors +3 -0
  44. model-00026-of-00044.safetensors +3 -0
  45. model-00027-of-00044.safetensors +3 -0
  46. model-00028-of-00044.safetensors +3 -0
  47. model-00029-of-00044.safetensors +3 -0
  48. model-00030-of-00044.safetensors +3 -0
  49. model-00031-of-00044.safetensors +3 -0
  50. model-00032-of-00044.safetensors +3 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model.safetensors.index.json filter=lfs diff=lfs merge=lfs -text
37
+ quantization_config.json filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
config.json ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DeepseekV41ForCausalLM"
4
+ ],
5
+ "model_type": "deepseek_v41",
6
+ "dtype": "bfloat16",
7
+ "transformers_version": "5.6.0",
8
+ "bos_token_id": 0,
9
+ "eos_token_id": 1,
10
+ "pad_token_id": 2,
11
+ "image_token_id": 129264,
12
+ "engram_table_dir": null,
13
+ "quantization_config": {
14
+ "quant_method": "exl3",
15
+ "version": "1.6.2",
16
+ "avg_bits": 3.5,
17
+ "head_bits": 6,
18
+ "calibration": {
19
+ "rows": 250,
20
+ "cols": 2048
21
+ },
22
+ "out_scales": "always",
23
+ "codebook": "mul1",
24
+ "original_quantization_config": {
25
+ "quant_method": "fp8",
26
+ "activation_scheme": "dynamic",
27
+ "weight_block_size": [
28
+ 32,
29
+ 32
30
+ ],
31
+ "scale_fmt": "ue8m0",
32
+ "expert_dtype": "fp4"
33
+ },
34
+ "mtp_bits": 6
35
+ },
36
+ "text_config": {
37
+ "model_type": "deepseek_v41_text",
38
+ "vocab_size": 129280,
39
+ "hidden_size": 5120,
40
+ "moe_intermediate_size": 2304,
41
+ "num_hidden_layers": 40,
42
+ "num_attention_heads": 64,
43
+ "num_key_value_heads": 1,
44
+ "head_dim": 512,
45
+ "qk_rope_head_dim": 64,
46
+ "q_lora_rank": 1280,
47
+ "o_lora_rank": 1024,
48
+ "o_groups": 8,
49
+ "hidden_act": "silu",
50
+ "swiglu_limit": 10.0,
51
+ "rms_norm_eps": 1e-20,
52
+ "attention_bias": false,
53
+ "attention_dropout": 0.0,
54
+ "initializer_range": 0.02,
55
+ "use_cache": true,
56
+ "tie_word_embeddings": false,
57
+ "max_position_embeddings": 1048576,
58
+ "rope_theta": 10000,
59
+ "rope_scaling": {
60
+ "rope_type": "yarn",
61
+ "factor": 16,
62
+ "beta_fast": 32,
63
+ "beta_slow": 1,
64
+ "original_max_position_embeddings": 65536
65
+ },
66
+ "n_routed_experts": 384,
67
+ "n_shared_experts": 1,
68
+ "num_experts_per_tok": 6,
69
+ "scoring_func": "sqrtsoftplus",
70
+ "topk_method": "noaux_tc",
71
+ "norm_topk_prob": true,
72
+ "routed_scaling_factor": 1.5,
73
+ "sliding_window": 128,
74
+ "compress_ratios": [
75
+ 0,
76
+ 0,
77
+ 2,
78
+ 2,
79
+ 2,
80
+ 2,
81
+ 2,
82
+ 2,
83
+ 2,
84
+ 2,
85
+ 2,
86
+ 2,
87
+ 2,
88
+ 2,
89
+ 2,
90
+ 2,
91
+ 2,
92
+ 2,
93
+ 2,
94
+ 2,
95
+ 1,
96
+ 1,
97
+ 1,
98
+ 1,
99
+ 1,
100
+ 1,
101
+ 1,
102
+ 1,
103
+ 1,
104
+ 1,
105
+ 1,
106
+ 1,
107
+ 1,
108
+ 1,
109
+ 1,
110
+ 1,
111
+ 1,
112
+ 1,
113
+ 1,
114
+ 1,
115
+ 0,
116
+ 0,
117
+ 0
118
+ ],
119
+ "compress_rope_theta": 160000,
120
+ "kv_source_layer_ids": [
121
+ 2,
122
+ 8,
123
+ 14,
124
+ 20
125
+ ],
126
+ "index_source_layer_ids": [
127
+ 2,
128
+ 8,
129
+ 14,
130
+ 20,
131
+ 24,
132
+ 28,
133
+ 32,
134
+ 36
135
+ ],
136
+ "index_n_heads": 32,
137
+ "index_head_dim": 128,
138
+ "index_topk": 512,
139
+ "candidate_source_layer_id": 20,
140
+ "candidate_topk_blocks": 2048,
141
+ "candidate_block_size": 8,
142
+ "hc_mult": 4,
143
+ "hc_sinkhorn_iters": 20,
144
+ "hc_eps": 1e-06,
145
+ "engram_layer_ids": [
146
+ 1,
147
+ 14
148
+ ],
149
+ "engram_num_embeddings": [
150
+ 384006168,
151
+ 384016682
152
+ ],
153
+ "engram_max_ngram_size": 4,
154
+ "engram_vocab_size": 16000000,
155
+ "engram_n_heads": 8,
156
+ "engram_head_dim": 256,
157
+ "engram_pad_token_id": 2,
158
+ "engram_compressed_vocab_size": 99092,
159
+ "num_nextn_predict_layers": 3,
160
+ "dspark_block_size": 5,
161
+ "dspark_noise_token_id": 128799,
162
+ "dspark_target_layer_ids": [
163
+ 37,
164
+ 38,
165
+ 39
166
+ ],
167
+ "dspark_markov_rank": 256,
168
+ "dspark_n_routed_experts": 128,
169
+ "dspark_num_experts_per_tok": 3
170
+ },
171
+ "vision_config": {
172
+ "model_type": "deepseek_v41_vision",
173
+ "num_hidden_layers": 32,
174
+ "hidden_size": 1024,
175
+ "num_attention_heads": 16,
176
+ "intermediate_size": 2816,
177
+ "patch_size": 14,
178
+ "rope_theta": 10000,
179
+ "downsample_ratio": 3,
180
+ "max_image_tokens": 1024,
181
+ "min_pixels": 295936,
182
+ "max_wh_ratio": null
183
+ }
184
+ }
encoding/README.md ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DeepSeek-V4.1 text and vision encoding
2
+
3
+ `encoding.py` is the standalone prompt-format reference for DeepSeek-V4.1. It
4
+ supports multi-turn conversations, tool calls, thinking modes, numeric reasoning
5
+ effort, mid-conversation system messages, and interleaved image content blocks,
6
+ without importing the inference implementation.
7
+
8
+ ## V4.1 changes relative to V4
9
+
10
+ Three prompt-format changes distinguish V4.1 from V4:
11
+
12
+ 1. **DSML tag names use a leading space.** Tool calls are wrapped in
13
+ `<|DSML| calls>` blocks with `<|DSML| invoke>` / `<|DSML| parameter>` tags
14
+ (note the space before `calls`, `invoke`, and `parameter`). The V4 format used
15
+ `<|DSML|tool_calls>` without a space.
16
+
17
+ 2. **Reasoning effort is a numeric budget (1–100).** The effort prefix is
18
+ rendered as `Reasoning Effort: {budget} (range 1-100, ...)` rather than the
19
+ verbose natural-language descriptions used in V4. String aliases map as
20
+ follows: `"low"` → 50, `"high"` → 75, `"max"` → 100. The
21
+ default is `"high"` (75). The effort prefix is only rendered in
22
+ `thinking_mode="thinking"` and only at the beginning of the conversation
23
+ (index 0).
24
+
25
+ 3. **Mid-conversation system messages** are supported via the `<|System|>` token.
26
+ A mid-conversation system message behaves like a user message for the purpose
27
+ of appending the assistant generation header.
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ from encoding import encode_messages, parse_message_from_completion_text
33
+
34
+ # Text-only conversation
35
+ messages = [
36
+ {"role": "system", "content": "You are a helpful assistant."},
37
+ {"role": "user", "content": "What is 2+2?"},
38
+ ]
39
+ prompt, media = encode_messages(
40
+ messages,
41
+ thinking_mode="thinking",
42
+ reasoning_effort=75, # integer 1–100, or "low"/"high"/"max"
43
+ return_multi_modal_data=True,
44
+ )
45
+ # prompt:
46
+ # '<|begin▁of▁sentence|><|System|>Reasoning Effort: 75 (range 1-100, the higher the
47
+ # value, the more thorough the reasoning)\n\nYou are a helpful assistant.
48
+ # <|User|>What is 2+2?<|Assistant|><think>'
49
+
50
+ # Parse model output back to a structured message
51
+ completion = "Simple arithmetic.</think>2 + 2 = 4.<|end▁of▁sentence|>"
52
+ parsed = parse_message_from_completion_text(completion, thinking_mode="thinking")
53
+ # => {"role": "assistant", "reasoning_content": "Simple arithmetic.",
54
+ # "content": "2 + 2 = 4.", "tool_calls": []}
55
+ ```
56
+
57
+ > **Note:** `parse_message_from_completion_text` is designed to handle
58
+ > well-formatted model output only. It does not attempt to correct or recover
59
+ > from malformed output that the model might occasionally generate. For
60
+ > production use, additional error handling is recommended.
61
+
62
+ ## OpenAI-style messages
63
+
64
+ ```python
65
+ from encoding import encode_messages
66
+
67
+ messages = [{
68
+ "role": "user",
69
+ "content": [
70
+ {"type": "text", "text": "第一张图"},
71
+ {
72
+ "type": "image_url",
73
+ "image_url": {"url": "examples/images/image_1.jpeg"},
74
+ },
75
+ {"type": "text", "text": "有什么内容?"},
76
+ ],
77
+ }]
78
+
79
+ prompt, media = encode_messages(
80
+ messages,
81
+ thinking_mode="chat",
82
+ return_multi_modal_data=True,
83
+ )
84
+ # prompt:
85
+ # '<|begin▁of▁sentence|><|User|>第一张图\n\n<|deepseek_image|>\n\n有什么内容?<|Assistant|></think>'
86
+ # media["images"] contains the image records in prompt order
87
+ ```
88
+
89
+ Images are represented in the prompt by `<|deepseek_image|>`. `media["images"]`
90
+ contains the corresponding image records in exactly the same order they appear in
91
+ the prompt. Pixel loading and expansion into model image tokens are handled by
92
+ `inference/image_processor.py`.
93
+
94
+ ## Compact TXT notation
95
+
96
+ `parse_tagged_text()` converts a compact prompt such as
97
+
98
+ ```text
99
+ 第一张图<image>examples/images/image_1.jpeg</image>有什么内容?
100
+ ```
101
+
102
+ into the same standard content blocks. It is an input convenience layer, not a
103
+ second encoding implementation.
104
+
105
+ ## Message format
106
+
107
+ ### Special tokens
108
+
109
+ | Token | Purpose |
110
+ | :--- | :--- |
111
+ | `<|begin▁of▁sentence|>` | Beginning of sequence (BOS) |
112
+ | `<|end▁of▁sentence|>` | End of assistant turn (EOS) |
113
+ | `<|User|>` | User turn prefix |
114
+ | `<|Assistant|>` | Assistant turn prefix |
115
+ | `<|System|>` | Mid-conversation system message prefix |
116
+ | `<|latest_reminder|>` | Latest reminder (date, locale, etc.) |
117
+ | `<think>` / `</think>` | Reasoning block delimiters |
118
+ | `|DSML|` | DSML markup token |
119
+ | `<|deepseek_image|>` | Image placeholder in the prompt string |
120
+
121
+ ### Roles
122
+
123
+ The encoding supports the following message roles: `system`, `user`, `assistant`,
124
+ `tool`, and `latest_reminder`.
125
+
126
+ A `tool` message is not rendered directly: `merge_tool_messages()` converts it
127
+ into a `<tool_result>` block inside the preceding user message. When multiple
128
+ tool results are present, they are sorted by the order of the corresponding
129
+ `tool_calls` in the preceding assistant message.
130
+
131
+ ### Basic chat
132
+
133
+ A simple multi-turn conversation is encoded as:
134
+
135
+ ```
136
+ <|begin▁of▁sentence|>{system_prompt}
137
+ <|User|>{user_message}<|Assistant|></think>{response}<|end▁of▁sentence|>
138
+ <|User|>{user_message_2}<|Assistant|></think>{response_2}<|end▁of▁sentence|>
139
+ ```
140
+
141
+ - The BOS token is prepended at the very beginning of the conversation.
142
+ - In **chat mode** (`thinking_mode="chat"`), `</think>` is placed right after
143
+ `<|Assistant|>` to immediately close the thinking block, so the model generates
144
+ content directly.
145
+
146
+ ### Thinking mode
147
+
148
+ In **thinking mode** (`thinking_mode="thinking"`), the model produces explicit
149
+ reasoning inside `<think>...</think>` blocks before responding.
150
+
151
+ ```
152
+ <|begin▁of▁sentence|><|System|>{reasoning_effort_prefix}{system_prompt}
153
+ <|User|>{message}<|Assistant|><think>{reasoning}</think>{response}<|end▁of▁sentence|>
154
+ ```
155
+
156
+ The reasoning effort prefix is injected once, before the system message, as a
157
+ `<|System|>` block:
158
+
159
+ ```
160
+ <|System|>Reasoning Effort: {budget} (range 1-100, the higher the value, the more thorough the reasoning)
161
+ ```
162
+
163
+ The `drop_thinking` parameter (default `True`) controls whether reasoning from
164
+ earlier turns is preserved:
165
+
166
+ - **Without a tools declaration**: reasoning before the latest user request is
167
+ stripped. Tool results alone do not start a new user turn, so reasoning within
168
+ the current turn is retained across tool calls.
169
+ - **With a tools declaration**: `drop_thinking` is automatically disabled. All
170
+ turns retain their reasoning, because tool-calling conversations require full
171
+ context for the model to track multi-step reasoning across tool calls.
172
+
173
+ ### Tool calling (DSML format)
174
+
175
+ Tools are defined on the `system` message via the `tools` field
176
+ (OpenAI-compatible format). When tools are present, the following schema block is
177
+ injected into the system prompt:
178
+
179
+ ```
180
+ ## Tools
181
+
182
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<|DSML| calls>" block like the following:
183
+
184
+ <|DSML| calls>
185
+ <|DSML| invoke name="$TOOL_NAME">
186
+ <|DSML| parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</|DSML| parameter>
187
+ ...
188
+ </|DSML| invoke>
189
+ <|DSML| invoke name="$TOOL_NAME2">
190
+ ...
191
+ </|DSML| invoke>
192
+ </|DSML| calls>
193
+
194
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
195
+
196
+ If thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
197
+
198
+ Otherwise, output directly after </think> with tool calls or final response.
199
+
200
+ ### Available Tool Schemas
201
+
202
+ {tool_definitions_json}
203
+
204
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
205
+ ```
206
+
207
+ An actual tool call in the assistant turn looks like:
208
+
209
+ ```xml
210
+
211
+ <|DSML| calls>
212
+ <|DSML| invoke name="function_name">
213
+ <|DSML| parameter name="param" string="true">string_value</|DSML| parameter>
214
+ <|DSML| parameter name="count" string="false">5</|DSML| parameter>
215
+ </|DSML| invoke>
216
+ </|DSML| calls><|end▁of▁sentence|>
217
+ ```
218
+
219
+ - `string="true"`: the parameter value is a raw string.
220
+ - `string="false"`: the parameter value is JSON (number, boolean, array, object).
221
+
222
+ Tool execution results are wrapped in `<tool_result>` tags within user messages:
223
+
224
+ ```
225
+ <|User|><tool_result>{result_json}</tool_result><|Assistant|><think>...
226
+ ```
227
+
228
+ ### Tool namespaces
229
+
230
+ Tool definitions may include a `namespace` alongside `function`, either as a
231
+ string or as an object with `name` and an optional `description`:
232
+
233
+ ```python
234
+ tool = {
235
+ "type": "function",
236
+ "namespace": {"name": "search", "description": "Search tools."},
237
+ "function": {
238
+ "name": "lookup",
239
+ "description": "Look up a value",
240
+ "parameters": {"type": "object", "properties": {"query": {"type": "string"}}},
241
+ },
242
+ }
243
+ tool_call = {
244
+ "type": "function",
245
+ "namespace": "search",
246
+ "function": {"name": "lookup", "arguments": '{"query": "value"}'},
247
+ }
248
+ ```
249
+
250
+ The tool schema and DSML invocation both use `search::lookup`. The namespace
251
+ description is prepended to the tool description, separated by a newline.
252
+ The parser returns `function.name="lookup"` and `namespace="search"` on the
253
+ tool call, so its output can be passed back to `encode_messages()` directly.
254
+
255
+ Input also accepts `namespace` inside `function`, or a qualified function name
256
+ such as `search::lookup`. A qualified name must agree with any explicit
257
+ namespace; `::` separates exactly one namespace from the tool name. Tools
258
+ without a namespace retain their original names and output format.
259
+
260
+ ### Reasoning effort
261
+
262
+ Pass `reasoning_effort` as an integer in `[1, 100]` or as one of `"low"` (50),
263
+ `"high"` (75), or `"max"` (100). The default is `"high"` (75).
264
+ The setting only affects `thinking_mode="thinking"` and is only rendered at the
265
+ start of the conversation (index 0). Intermediate values may be used to elicit
266
+ interpolated reasoning behavior.
267
+
268
+ ### Quick instruction special tokens
269
+
270
+ Quick instruction tokens are used for auxiliary classification and generation
271
+ tasks. They are appended to messages via the `"task"` field to trigger
272
+ specialized model behavior for a single-token or short-form output.
273
+
274
+ | Special Token | Description | Format |
275
+ |:---|:---|:---|
276
+ | `<|action|>` | Determines whether the user prompt requires a web search or can be answered directly. | `...<|User|>{prompt}<|Assistant|><think><|action|>` |
277
+ | `<|title|>` | Generates a concise conversation title after the first assistant response. | `...<|Assistant|>{response}<|end▁of▁sentence|><|title|>` |
278
+ | `<|query|>` | Generates search queries for the user prompt. | `...<|User|>{prompt}<|query|>` |
279
+ | `<|authority|>` | Classifies the user prompt's demand for source authoritativeness. | `...<|User|>{prompt}<|authority|>` |
280
+ | `<|domain|>` | Identifies the domain of the user prompt. | `...<|User|>{prompt}<|domain|>` |
281
+ | `<|read_url|>` | Determines whether each URL in the user prompt should be fetched and read. | `...<|User|>{prompt}<|read_url|>` |
282
+
283
+ Usage in message format:
284
+
285
+ - **`action`** on a user message: the `<|action|>` token is placed after the
286
+ assistant prefix and thinking token, triggering a routing decision (e.g.,
287
+ "Search" or "Answer").
288
+ - **Other tasks** (`query`, `authority`, `domain`, `read_url`) on a user message:
289
+ the task token is appended directly after the user content.
290
+ - **`title`** on an assistant message: the `<|title|>` token is appended after
291
+ the assistant's EOS. The next assistant message provides the generated title.
292
+
293
+ ## Tests
294
+
295
+ From this directory:
296
+
297
+ ```bash
298
+ python -m pytest -q test_encoding.py
299
+ ```
300
+
301
+ Test cases are stored as paired JSON input / TXT expected-output files under
302
+ `tests/`. The tests cover multi-turn conversations, tool calling, thinking mode,
303
+ numeric reasoning effort, mid-conversation system messages, and multimodal image
304
+ ordering. They include a check that the TXT and JSON examples encode to the same
305
+ prompt and preserve the same image ordering.
encoding/__pycache__/encoding.cpython-312.pyc ADDED
Binary file (39.6 kB). View file
 
encoding/encoding.py ADDED
@@ -0,0 +1,990 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepSeek-V4.1 Text and Vision Encoding
3
+
4
+ A fully self-contained implementation for encoding/decoding DeepSeek-V4.1 chat
5
+ messages with tool calling, thinking mode, quick instruction tasks, and image
6
+ content blocks. No dependency on encoding_dsv4.
7
+
8
+ V4.1 changes relative to V4:
9
+
10
+ 1. DSML tag names: tool calls are wrapped in "<|DSML| calls>" blocks with
11
+ "<|DSML| invoke>" / "<|DSML| parameter>" tags (leading-space tag names).
12
+ 2. Numeric reasoning effort: "Reasoning Effort: {budget} (range 1-100, ...)".
13
+ Accepts an int in [1, 100] or one of "low"/"high"/"max"
14
+ (mapped to 50/75/100). Defaults to "high". Only rendered in thinking mode.
15
+ 3. Mid-conversation system messages are supported via the "<|System|>" token.
16
+ A mid-conversation system message behaves like a user message for the purpose
17
+ of appending the assistant generation header.
18
+ """
19
+
20
+ from typing import Any, Dict, List, Union, Optional, Tuple
21
+ import copy
22
+ import json
23
+ import re
24
+
25
+ # ============================================================
26
+ # Special Tokens
27
+ # ============================================================
28
+
29
+ bos_token: str = "<|begin▁of▁sentence|>"
30
+ eos_token: str = "<|end▁of▁sentence|>"
31
+ thinking_start_token: str = "<think>"
32
+ thinking_end_token: str = "</think>"
33
+ dsml_token: str = "|DSML|"
34
+
35
+ USER_SP_TOKEN = "<|User|>"
36
+ ASSISTANT_SP_TOKEN = "<|Assistant|>"
37
+ LATEST_REMINDER_SP_TOKEN = "<|latest_reminder|>"
38
+
39
+ IMAGE_PLACEHOLDER = "<|deepseek_image|>"
40
+ IMAGE_TAG_PATTERN = re.compile(r"<image>(.*?)</image>", re.DOTALL)
41
+
42
+ # Task special tokens for internal classification tasks
43
+ DS_TASK_SP_TOKENS = {
44
+ "action": "<|action|>",
45
+ "query": "<|query|>",
46
+ "authority": "<|authority|>",
47
+ "domain": "<|domain|>",
48
+ "title": "<|title|>",
49
+ "read_url": "<|read_url|>",
50
+ }
51
+ VALID_TASKS = set(DS_TASK_SP_TOKENS.keys())
52
+
53
+ # ============================================================
54
+ # Templates
55
+ # ============================================================
56
+
57
+ system_msg_template: str = "{content}"
58
+ user_msg_template: str = "{content}"
59
+ latest_reminder_msg_template: str = "{content}"
60
+ assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
61
+ assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
62
+ thinking_template: str = "{reasoning_content}"
63
+
64
+ response_format_template: str = (
65
+ "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
66
+ )
67
+
68
+ tool_output_template: str = (
69
+ "<tool_result>{content}</tool_result>"
70
+ )
71
+
72
+ # ============================================================
73
+ # Utility Functions
74
+ # ============================================================
75
+
76
+ def to_json(value: Any) -> str:
77
+ """Serialize a value to JSON string."""
78
+ try:
79
+ return json.dumps(value, ensure_ascii=False)
80
+ except:
81
+ return json.dumps(value, ensure_ascii=True)
82
+
83
+
84
+ def tools_from_openai_format(tools):
85
+ """Extract function definitions with namespace-qualified names."""
86
+ functions = []
87
+ for tool in tools:
88
+ function = dict(tool["function"])
89
+ if tool.get("namespace") is not None:
90
+ function["namespace"] = tool["namespace"]
91
+ function["name"] = _tool_name_for_encoding(function)
92
+ namespace = function.pop("namespace", None)
93
+ if isinstance(namespace, dict) and namespace.get("description"):
94
+ function["description"] = (
95
+ namespace["description"] + "\n" + (function.get("description") or "")
96
+ )
97
+ functions.append(function)
98
+ return functions
99
+
100
+
101
+ def _split_tool_name(name: str, namespace: Optional[str] = None) -> Tuple[Optional[str], str]:
102
+ """Split a qualified name and validate any explicit namespace."""
103
+ prefix, separator, bare_name = name.partition("::")
104
+ if separator:
105
+ assert namespace in (None, prefix), (
106
+ f"Conflicting tool namespaces: {namespace} != {prefix}"
107
+ )
108
+ namespace, name = prefix, bare_name
109
+ assert "::" not in name, f"Tool name must not contain '::': {name}"
110
+ assert namespace is None or "::" not in namespace, (
111
+ f"Tool namespace must not contain '::': {namespace}"
112
+ )
113
+ return namespace, name
114
+
115
+
116
+ def _tool_name_for_encoding(tool: Dict[str, Any]) -> str:
117
+ namespace = tool.get("namespace")
118
+ if isinstance(namespace, dict):
119
+ namespace = namespace["name"]
120
+ namespace, name = _split_tool_name(tool["name"], namespace)
121
+ return name if namespace is None else f"{namespace}::{name}"
122
+
123
+
124
+ def tool_calls_from_openai_format(tool_calls):
125
+ """Convert OpenAI-format tool calls to internal format."""
126
+ calls = []
127
+ for tool_call in tool_calls:
128
+ function = tool_call["function"]
129
+ namespace, name = _split_tool_name(
130
+ function["name"], tool_call.get("namespace") or function.get("namespace")
131
+ )
132
+ call = {"name": name, "arguments": function["arguments"]}
133
+ if namespace is not None:
134
+ call["namespace"] = namespace
135
+ calls.append(call)
136
+ return calls
137
+
138
+
139
+ def tool_calls_to_openai_format(tool_calls):
140
+ """Convert internal tool calls to OpenAI format."""
141
+ calls = []
142
+ for tool_call in tool_calls:
143
+ call = {
144
+ "type": "function",
145
+ "function": {
146
+ "name": tool_call["name"],
147
+ "arguments": tool_call["arguments"],
148
+ }
149
+ }
150
+ if tool_call.get("namespace") is not None:
151
+ call["namespace"] = tool_call["namespace"]
152
+ calls.append(call)
153
+ return calls
154
+
155
+
156
+ def decode_dsml_to_arguments(tool_name: str, tool_args: Dict[str, Tuple[str, str]]) -> Dict[str, str]:
157
+ """
158
+ Decode DSML parameters back to a tool call dict.
159
+
160
+ Args:
161
+ tool_name: Name of the tool.
162
+ tool_args: Dict mapping param_name -> (value, is_string_flag).
163
+
164
+ Returns:
165
+ Dict with "name", "arguments" (JSON string), and optional "namespace".
166
+ """
167
+ def _decode_value(key: str, value: str, string: str):
168
+ if string == "true":
169
+ value = to_json(value)
170
+ return f"{to_json(key)}: {value}"
171
+
172
+ tool_args_json = "{" + ", ".join([_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]) + "}"
173
+ namespace, name = _split_tool_name(tool_name)
174
+ tool_call = dict(name=name, arguments=tool_args_json)
175
+ if namespace is not None:
176
+ tool_call["namespace"] = namespace
177
+ return tool_call
178
+
179
+
180
+ # ============================================================
181
+ # Preprocessing
182
+ # ============================================================
183
+
184
+ def merge_tool_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
185
+ """
186
+ Merge tool messages into the preceding user message using content_blocks format.
187
+
188
+ DeepSeek-V4.1 does not have a standalone "tool" role; instead, tool results
189
+ are encoded as <tool_result> blocks within user messages.
190
+ """
191
+ merged: List[Dict[str, Any]] = []
192
+
193
+ for msg in messages:
194
+ msg = copy.deepcopy(msg)
195
+ role = msg.get("role")
196
+
197
+ if role == "tool":
198
+ # Convert tool message to a user message with tool_result block
199
+ content = msg.get("content", "")
200
+ if msg.get("content_blocks") and not isinstance(msg.get("content"), str):
201
+ content = msg["content_blocks"]
202
+ tool_block = {
203
+ "type": "tool_result",
204
+ "tool_use_id": msg.get("tool_call_id", ""),
205
+ "content": content,
206
+ }
207
+ # Merge into previous message if it's already a user (merged tool)
208
+ if merged and merged[-1].get("role") == "user" and "content_blocks" in merged[-1]:
209
+ merged[-1]["content_blocks"].append(tool_block)
210
+ else:
211
+ merged.append({
212
+ "role": "user",
213
+ "content_blocks": [tool_block],
214
+ })
215
+ elif role == "user":
216
+ content_blocks = msg.get("content_blocks")
217
+ if content_blocks is None:
218
+ content = msg.get("content", "")
219
+ content_blocks = content if isinstance(content, list) else [{"type": "text", "text": content}]
220
+ if merged and merged[-1].get("role") == "user" and "content_blocks" in merged[-1] and merged[-1].get("task") is None:
221
+ merged[-1]["content_blocks"].extend(content_blocks)
222
+ else:
223
+ # Preserve structured content and all message-level metadata.
224
+ new_msg = msg
225
+ new_msg["content_blocks"] = content_blocks
226
+ merged.append(new_msg)
227
+ else:
228
+ merged.append(msg)
229
+
230
+ return merged
231
+
232
+
233
+ def sort_tool_results_by_call_order(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
234
+ """
235
+ Sort tool_result blocks within user messages by the order of tool_calls
236
+ in the preceding assistant message.
237
+ """
238
+ last_tool_call_order: Dict[str, int] = {}
239
+
240
+ for msg in messages:
241
+ role = msg.get("role")
242
+ if role == "assistant" and msg.get("tool_calls"):
243
+ last_tool_call_order = {}
244
+ for idx, tc in enumerate(msg["tool_calls"]):
245
+ tc_id = tc.get("id") or tc.get("function", {}).get("id", "")
246
+ if tc_id:
247
+ last_tool_call_order[tc_id] = idx
248
+
249
+ elif role == "user" and msg.get("content_blocks"):
250
+ tool_blocks = [b for b in msg["content_blocks"] if b.get("type") == "tool_result"]
251
+ if len(tool_blocks) > 1 and last_tool_call_order:
252
+ sorted_blocks = sorted(
253
+ tool_blocks,
254
+ key=lambda b: last_tool_call_order.get(b.get("tool_use_id", ""), 0)
255
+ )
256
+ sorted_idx = 0
257
+ new_blocks = []
258
+ for block in msg["content_blocks"]:
259
+ if block.get("type") == "tool_result":
260
+ new_blocks.append(sorted_blocks[sorted_idx])
261
+ sorted_idx += 1
262
+ else:
263
+ new_blocks.append(block)
264
+ msg["content_blocks"] = new_blocks
265
+
266
+ return messages
267
+
268
+
269
+ # ============================================================
270
+ # Vision Message Preprocessing
271
+ # ============================================================
272
+
273
+ def parse_tagged_text(text: str) -> Union[str, List[Dict[str, Any]]]:
274
+ """Convert ``<image>path</image>`` text into standard content blocks."""
275
+ matches = list(IMAGE_TAG_PATTERN.finditer(text))
276
+ remaining = IMAGE_TAG_PATTERN.sub("", text)
277
+ if "<image>" in remaining or "</image>" in remaining:
278
+ raise ValueError("Malformed <image>path</image> tag")
279
+ if not matches:
280
+ return text
281
+
282
+ blocks: List[Dict[str, Any]] = []
283
+ cursor = 0
284
+ for match in matches:
285
+ if match.start() > cursor:
286
+ blocks.append({"type": "text", "text": text[cursor:match.start()]})
287
+ path = match.group(1)
288
+ if not path:
289
+ raise ValueError("Image path must not be empty")
290
+ blocks.append({
291
+ "type": "image_url",
292
+ "image_url": {"url": path},
293
+ })
294
+ cursor = match.end()
295
+ if cursor < len(text):
296
+ blocks.append({"type": "text", "text": text[cursor:]})
297
+ return blocks
298
+
299
+
300
+ def _is_image_block(block: Dict[str, Any]) -> bool:
301
+ """Return whether a content block is an OpenAI/Anthropic/internal image."""
302
+ return isinstance(block, dict) and block.get("type") in ("image", "image_url")
303
+
304
+
305
+ def _extract_image(block: Dict[str, Any]) -> Dict[str, Any]:
306
+ """Normalize a supported image block into an internal image record."""
307
+ record: Dict[str, Any] = {"type": "image"}
308
+ if block.get("type") == "image_url":
309
+ image_url = block.get("image_url")
310
+ if isinstance(image_url, str):
311
+ record["url"] = image_url
312
+ else:
313
+ record["url"] = (image_url or {}).get("url", "")
314
+ else:
315
+ for key in ("source", "url", "data"):
316
+ if key in block:
317
+ record[key] = block[key]
318
+ if not any(record.get(key) for key in ("source", "url", "data")):
319
+ raise ValueError("Image block does not contain a valid source")
320
+ return record
321
+
322
+
323
+ def _process_image_blocks(
324
+ blocks: List[Any], image_placeholder: str = IMAGE_PLACEHOLDER
325
+ ) -> Tuple[List[Any], List[Dict[str, Any]]]:
326
+ """Replace image blocks and collect their records in one ordered traversal."""
327
+ new_blocks: List[Any] = []
328
+ images: List[Dict[str, Any]] = []
329
+ for block in blocks:
330
+ if not isinstance(block, dict):
331
+ new_blocks.append(block)
332
+ continue
333
+ if _is_image_block(block):
334
+ new_blocks.append({"type": "text", "text": image_placeholder})
335
+ images.append(_extract_image(block))
336
+ elif block.get("type") == "tool_result" and isinstance(block.get("content"), list):
337
+ block = copy.copy(block)
338
+ block["content"], nested_images = _process_image_blocks(
339
+ block["content"], image_placeholder)
340
+ new_blocks.append(block)
341
+ images.extend(nested_images)
342
+ elif block.get("type") == "text":
343
+ text = block.get("text") or ""
344
+ if IMAGE_PLACEHOLDER in text:
345
+ raise ValueError(
346
+ f"Text block contains image placeholder '{IMAGE_PLACEHOLDER}': "
347
+ f"'{text[:100]}'. Images should be separate content blocks."
348
+ )
349
+ new_blocks.append(block)
350
+ else:
351
+ new_blocks.append(block)
352
+ return new_blocks, images
353
+
354
+
355
+ def _validate_no_image_sp_tokens(msg: Dict[str, Any]) -> None:
356
+ """Reject user-supplied image placeholder tokens in textual fields."""
357
+ content = msg.get("content")
358
+ if isinstance(content, str) and IMAGE_PLACEHOLDER in content:
359
+ raise ValueError(
360
+ f"Message content contains image special token '{IMAGE_PLACEHOLDER}'. "
361
+ "Images should be provided as image content blocks."
362
+ )
363
+ reasoning_content = msg.get("reasoning_content")
364
+ if isinstance(reasoning_content, str) and IMAGE_PLACEHOLDER in reasoning_content:
365
+ raise ValueError(
366
+ f"reasoning_content contains image special token '{IMAGE_PLACEHOLDER}'"
367
+ )
368
+
369
+
370
+ def process_image_messages(
371
+ messages: List[Dict[str, Any]],
372
+ ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
373
+ """Normalize image blocks and return their records in prompt order."""
374
+ processed: List[Dict[str, Any]] = []
375
+ images: List[Dict[str, Any]] = []
376
+ for msg in messages:
377
+ msg = copy.deepcopy(msg)
378
+ _validate_no_image_sp_tokens(msg)
379
+
380
+ if isinstance(msg.get("content"), list) and "content_blocks" not in msg:
381
+ msg["content_blocks"] = msg.pop("content")
382
+
383
+ if msg.get("content_blocks"):
384
+ msg["content_blocks"], message_images = _process_image_blocks(
385
+ msg["content_blocks"])
386
+ images.extend(message_images)
387
+ if not isinstance(msg.get("content"), str):
388
+ texts = [
389
+ block.get("text", "")
390
+ for block in msg["content_blocks"]
391
+ if isinstance(block, dict) and block.get("type") == "text"
392
+ ]
393
+ msg["content"] = "\n\n".join(texts)
394
+
395
+ processed.append(msg)
396
+ return processed, images
397
+
398
+
399
+ def _read_until_stop(index: int, text: str, stop: List[str]) -> Tuple[int, str, Optional[str]]:
400
+ """
401
+ Read text from index until one of the stop strings is found.
402
+
403
+ Returns:
404
+ Tuple of (new_index, content_before_stop, matched_stop_string_or_None).
405
+ """
406
+ min_pos = len(text)
407
+ matched_stop = None
408
+
409
+ for s in stop:
410
+ pos = text.find(s, index)
411
+ if pos != -1 and pos < min_pos:
412
+ min_pos = pos
413
+ matched_stop = s
414
+
415
+ if matched_stop:
416
+ content = text[index:min_pos]
417
+ return min_pos + len(matched_stop), content, matched_stop
418
+ else:
419
+ content = text[index:]
420
+ return len(text), content, None
421
+
422
+ # ============================================================
423
+ # V4.1 Special Tokens and DSML Tag Names
424
+ # ============================================================
425
+
426
+ SYSTEM_SP_TOKEN = "<|System|>"
427
+
428
+ tool_calls_block_name: str = " calls"
429
+ tool_call_tag_name: str = " invoke"
430
+ tool_parameter_tag_name: str = " parameter"
431
+
432
+ tool_call_template: str = (
433
+ "<{dsml_token}{tool_call_tag_name} name=\"{name}\">\n{arguments}\n</{dsml_token}{tool_call_tag_name}>"
434
+ )
435
+ tool_calls_template = (
436
+ "<{dsml_token}{tc_block_name}>\n{tool_calls}\n</{dsml_token}{tc_block_name}>"
437
+ )
438
+
439
+ # ============================================================
440
+ # Reasoning Effort (numeric budget)
441
+ # ============================================================
442
+
443
+ REASONING_EFFORT_TEMPLATE = (
444
+ "Reasoning Effort: {budget} "
445
+ "(range 1-100, the higher the value, the more thorough the reasoning)\n\n"
446
+ )
447
+
448
+ REASONING_EFFORT_MAPPINGS: Dict[str, int] = {
449
+ "low": 50,
450
+ "high": 75,
451
+ "max": 100,
452
+ }
453
+ DEFAULT_REASONING_EFFORT = "high"
454
+
455
+
456
+ def render_reasoning_effort(
457
+ index: int,
458
+ thinking_mode: str,
459
+ effort: Union[str, int, None],
460
+ ) -> str:
461
+ """Render the V4.1 numeric reasoning effort prefix (thinking mode, index 0 only)."""
462
+ if effort is None:
463
+ effort = DEFAULT_REASONING_EFFORT
464
+ assert (
465
+ type(effort) is int and 1 <= effort <= 100
466
+ ) or effort in REASONING_EFFORT_MAPPINGS, (
467
+ "Invalid reasoning effort for deepseek_v41: "
468
+ f"{effort}, should be int within [1,100] or {list(REASONING_EFFORT_MAPPINGS)}"
469
+ )
470
+ if type(effort) is str:
471
+ effort = REASONING_EFFORT_MAPPINGS[effort]
472
+ if index == 0 and thinking_mode == "thinking":
473
+ return REASONING_EFFORT_TEMPLATE.format(budget=effort)
474
+ return ""
475
+
476
+
477
+ # ============================================================
478
+ # Tools rendering
479
+ # ============================================================
480
+
481
+ TOOLS_TEMPLATE = """## Tools
482
+
483
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}{tc_block_name}>" block like the following:
484
+
485
+ <{dsml_token}{tc_block_name}>
486
+ <{dsml_token}{tool_call_tag_name} name="$TOOL_NAME">
487
+ <{dsml_token}{tool_parameter_tag_name} name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}{tool_parameter_tag_name}>
488
+ ...
489
+ </{dsml_token}{tool_call_tag_name}>
490
+ <{dsml_token}{tool_call_tag_name} name="$TOOL_NAME2">
491
+ ...
492
+ </{dsml_token}{tool_call_tag_name}>
493
+ </{dsml_token}{tc_block_name}>
494
+
495
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
496
+
497
+ If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
498
+
499
+ Otherwise, output directly after {thinking_end_token} with tool calls or final response.
500
+
501
+ ### Available Tool Schemas
502
+
503
+ {tool_schemas}
504
+
505
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
506
+ """
507
+
508
+
509
+ def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
510
+ """Render tool schemas into the V4.1 system prompt format."""
511
+ tools_json = [to_json(t) for t in tools]
512
+
513
+ return TOOLS_TEMPLATE.format(
514
+ tool_schemas="\n".join(tools_json),
515
+ dsml_token=dsml_token,
516
+ tc_block_name=tool_calls_block_name,
517
+ tool_call_tag_name=tool_call_tag_name,
518
+ tool_parameter_tag_name=tool_parameter_tag_name,
519
+ thinking_start_token=thinking_start_token,
520
+ thinking_end_token=thinking_end_token,
521
+ )
522
+
523
+
524
+ def encode_arguments_to_dsml(tool_call: Dict[str, Any]) -> str:
525
+ """Encode tool call arguments into V4.1 DSML parameter format."""
526
+ p_dsml_template = (
527
+ '<{dsml_token}{tool_parameter_tag_name} name="{key}" string="{is_str}">'
528
+ '{value}</{dsml_token}{tool_parameter_tag_name}>'
529
+ )
530
+ P_dsml_strs = []
531
+
532
+ arguments = tool_call["arguments"]
533
+ if not isinstance(arguments, dict):
534
+ # Tolerate JSON strings, including double-encoded ones.
535
+ for _ in range(2):
536
+ if isinstance(arguments, str):
537
+ try:
538
+ arguments = json.loads(arguments)
539
+ except Exception:
540
+ break
541
+ else:
542
+ break
543
+ if not isinstance(arguments, dict):
544
+ arguments = {"arguments": tool_call["arguments"]}
545
+
546
+ for k, v in arguments.items():
547
+ P_dsml_strs.append(p_dsml_template.format(
548
+ dsml_token=dsml_token,
549
+ tool_parameter_tag_name=tool_parameter_tag_name,
550
+ key=k,
551
+ is_str="true" if isinstance(v, str) else "false",
552
+ value=v if isinstance(v, str) else to_json(v),
553
+ ))
554
+
555
+ return "\n".join(P_dsml_strs)
556
+
557
+
558
+ # ============================================================
559
+ # Message Rendering
560
+ # ============================================================
561
+
562
+ def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
563
+ """
564
+ Find the index of the last user message.
565
+
566
+ V4.1 supports mid-conversation system messages, which count as user
567
+ messages for the purposes of the assistant generation header.
568
+ User messages containing only tool results do not start a new turn.
569
+ """
570
+ last_user_index = -1
571
+ for idx in range(len(messages) - 1, -1, -1):
572
+ role = messages[idx].get("role")
573
+ if role == "user" or (role == "system" and idx > 0):
574
+ content_blocks = messages[idx].get("content_blocks")
575
+ if content_blocks and not any(
576
+ block.get("type") in ("text", "image", "image_url")
577
+ for block in content_blocks
578
+ ):
579
+ continue
580
+ last_user_index = idx
581
+ break
582
+ return last_user_index
583
+
584
+
585
+ def render_message(
586
+ index: int,
587
+ messages: List[Dict[str, Any]],
588
+ thinking_mode: str,
589
+ drop_thinking: bool = True,
590
+ reasoning_effort: Union[str, int, None] = None,
591
+ ) -> str:
592
+ """
593
+ Render a single message at the given index into its V4.1 encoded string form.
594
+ """
595
+ assert 0 <= index < len(messages)
596
+ assert thinking_mode in ["chat", "thinking"], f"Invalid thinking_mode `{thinking_mode}`"
597
+
598
+ msg = messages[index]
599
+ last_user_idx = find_last_user_index(messages)
600
+
601
+ role = msg.get("role")
602
+ content = msg.get("content")
603
+ tools = msg.get("tools")
604
+ response_format = msg.get("response_format")
605
+ tool_calls = msg.get("tool_calls")
606
+ reasoning_content = msg.get("reasoning_content")
607
+ wo_eos = msg.get("wo_eos", False)
608
+
609
+ if tools:
610
+ tools = tools_from_openai_format(tools)
611
+ if tool_calls:
612
+ tool_calls = tool_calls_from_openai_format(tool_calls)
613
+
614
+ # Reasoning effort prefix (thinking mode, index 0 only)
615
+ reasoning_effort_prompt = render_reasoning_effort(index, thinking_mode, reasoning_effort)
616
+ # System token leads the conversation when there is a reasoning effort prompt
617
+ # or the first message is a system message.
618
+ prompt = SYSTEM_SP_TOKEN if index == 0 and (reasoning_effort_prompt or role == "system") else ""
619
+ prompt += reasoning_effort_prompt
620
+
621
+ if role == "system":
622
+ if index > 0:
623
+ # Mid-conversation system message
624
+ prompt += SYSTEM_SP_TOKEN
625
+ prompt += system_msg_template.format(content=content or "")
626
+ if tools:
627
+ prompt += "\n\n" + render_tools(tools)
628
+ if response_format:
629
+ prompt += "\n\n" + response_format_template.format(schema=to_json(response_format))
630
+
631
+ elif role == "user":
632
+ prompt += USER_SP_TOKEN
633
+
634
+ # Handle content blocks (tool results mixed with text)
635
+ content_blocks = msg.get("content_blocks")
636
+ if content_blocks:
637
+ parts = []
638
+ for block in content_blocks:
639
+ block_type = block.get("type")
640
+ if block_type == "text":
641
+ parts.append(block.get("text", ""))
642
+ elif block_type == "tool_result":
643
+ tool_content = block.get("content", "")
644
+ if isinstance(tool_content, list):
645
+ text_parts = []
646
+ for b in tool_content:
647
+ if b.get("type") == "text":
648
+ text_parts.append(b.get("text", ""))
649
+ else:
650
+ text_parts.append(f"[Unsupported {b.get('type')}]")
651
+ tool_content = "\n\n".join(text_parts)
652
+ parts.append(tool_output_template.format(content=tool_content))
653
+ else:
654
+ parts.append(f"[Unsupported {block_type}]")
655
+ prompt += "\n\n".join(parts)
656
+ else:
657
+ prompt += content or ""
658
+
659
+ elif role == "latest_reminder":
660
+ prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(content=content)
661
+
662
+ elif role == "tool":
663
+ raise NotImplementedError("deepseek_v41 merges tool messages into user; please preprocess with merge_tool_messages()")
664
+
665
+ elif role == "assistant":
666
+ thinking_part = ""
667
+ tc_content = ""
668
+
669
+ if tool_calls:
670
+ tc_list = [
671
+ tool_call_template.format(
672
+ dsml_token=dsml_token,
673
+ tool_call_tag_name=tool_call_tag_name,
674
+ name=_tool_name_for_encoding(tc),
675
+ arguments=encode_arguments_to_dsml(tc)
676
+ )
677
+ for tc in tool_calls
678
+ ]
679
+ tc_content += '\n\n' + tool_calls_template.format(
680
+ dsml_token=dsml_token,
681
+ tool_calls="\n".join(tc_list),
682
+ tc_block_name=tool_calls_block_name,
683
+ )
684
+
685
+ summary_content = content or ""
686
+ rc = reasoning_content or ""
687
+
688
+ # Check if previous message has a task - if so, this is a task output (no thinking)
689
+ prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
690
+
691
+ if thinking_mode == "thinking" and not prev_has_task:
692
+ if not drop_thinking or index > last_user_idx:
693
+ thinking_part = thinking_template.format(reasoning_content=rc) + thinking_end_token
694
+ else:
695
+ thinking_part = ""
696
+
697
+ if wo_eos:
698
+ prompt += assistant_msg_wo_eos_template.format(
699
+ reasoning=thinking_part,
700
+ content=summary_content,
701
+ tool_calls=tc_content,
702
+ )
703
+ else:
704
+ prompt += assistant_msg_template.format(
705
+ reasoning=thinking_part,
706
+ content=summary_content,
707
+ tool_calls=tc_content,
708
+ )
709
+ else:
710
+ raise NotImplementedError(f"Unknown role: {role}")
711
+
712
+ # Append transition tokens based on what follows
713
+ if index + 1 < len(messages) and messages[index + 1].get("role") not in ["assistant", "latest_reminder"]:
714
+ return prompt
715
+
716
+ task = messages[index].get("task")
717
+ if task is not None:
718
+ # Task special token for internal classification tasks
719
+ assert task in VALID_TASKS, f"Invalid task: '{task}'. Valid tasks are: {list(VALID_TASKS)}"
720
+ task_sp_token = DS_TASK_SP_TOKENS[task]
721
+
722
+ if task != "action":
723
+ # Non-action tasks: append task sp token directly after the message
724
+ prompt += task_sp_token
725
+ else:
726
+ # Action task: append Assistant + thinking token + action sp token
727
+ prompt += ASSISTANT_SP_TOKEN
728
+ prompt += thinking_end_token if thinking_mode != "thinking" else thinking_start_token
729
+ prompt += task_sp_token
730
+
731
+ elif role == "user" or (role == "system" and index > 0):
732
+ # Normal generation: append Assistant + thinking token
733
+ # (mid-conversation system messages also trigger the assistant header)
734
+ prompt += ASSISTANT_SP_TOKEN
735
+ if not drop_thinking and thinking_mode == "thinking":
736
+ prompt += thinking_start_token
737
+ elif drop_thinking and thinking_mode == "thinking" and index >= last_user_idx:
738
+ prompt += thinking_start_token
739
+ else:
740
+ prompt += thinking_end_token
741
+
742
+ return prompt
743
+
744
+
745
+ # ============================================================
746
+ # Main Encoding Function
747
+ # ============================================================
748
+
749
+ def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
750
+ """
751
+ Drop reasoning_content and non-essential messages before the last user message.
752
+ Same as V4, but uses the V4.1 last-user definition (mid systems count).
753
+ """
754
+ last_user_idx = find_last_user_index(messages)
755
+ result = []
756
+ keep_roles = {"user", "system", "tool", "latest_reminder", "direct_search_results"}
757
+
758
+ for idx, msg in enumerate(messages):
759
+ role = msg.get("role")
760
+ if role in keep_roles or idx >= last_user_idx:
761
+ result.append(msg)
762
+ elif role == "assistant":
763
+ msg = copy.copy(msg)
764
+ msg.pop("reasoning_content", None)
765
+ result.append(msg)
766
+
767
+ return result
768
+
769
+
770
+ def _encode_messages_text(
771
+ messages: List[Dict[str, Any]],
772
+ thinking_mode: str,
773
+ context: Optional[List[Dict[str, Any]]] = None,
774
+ drop_thinking: bool = True,
775
+ add_default_bos_token: bool = True,
776
+ reasoning_effort: Union[str, int, None] = None,
777
+ ) -> str:
778
+ """Encode preprocessed (text-only) messages into the V4.1 prompt format."""
779
+ context = context if context else []
780
+
781
+ full_messages = context + messages
782
+
783
+ prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
784
+
785
+ # Resolve drop_thinking: if any message has tools defined, don't drop thinking
786
+ effective_drop_thinking = drop_thinking
787
+ if any(m.get("tools") for m in full_messages):
788
+ effective_drop_thinking = False
789
+
790
+ if thinking_mode == "thinking" and effective_drop_thinking:
791
+ full_messages = _drop_thinking_messages(full_messages)
792
+ num_to_render = len(full_messages) - len(_drop_thinking_messages(context))
793
+ context_len = len(full_messages) - num_to_render
794
+ else:
795
+ num_to_render = len(messages)
796
+ context_len = len(context)
797
+
798
+ for idx in range(num_to_render):
799
+ prompt += render_message(
800
+ idx + context_len,
801
+ full_messages,
802
+ thinking_mode=thinking_mode,
803
+ drop_thinking=effective_drop_thinking,
804
+ reasoning_effort=reasoning_effort,
805
+ )
806
+
807
+ return prompt
808
+
809
+
810
+ def encode_messages(
811
+ messages: List[Dict[str, Any]],
812
+ thinking_mode: str,
813
+ context: Optional[List[Dict[str, Any]]] = None,
814
+ drop_thinking: bool = True,
815
+ add_default_bos_token: bool = True,
816
+ reasoning_effort: Union[str, int, None] = None,
817
+ return_multi_modal_data: bool = False,
818
+ ) -> Any:
819
+ """Encode text or multimodal messages into the DeepSeek-V4.1 prompt format.
820
+
821
+ Text-only calls return the prompt string. When return_multi_modal_data is
822
+ true, the result is ``(prompt, media_data)``.
823
+ """
824
+ # Validate before merging can discard message-level textual fields.
825
+ for msg in (context or []) + messages:
826
+ _validate_no_image_sp_tokens(msg)
827
+
828
+ # Collect images only after tool results are in their final prompt order.
829
+ context = merge_tool_messages(context or [])
830
+ messages = merge_tool_messages(messages)
831
+ messages = sort_tool_results_by_call_order(context + messages)[len(context):]
832
+ processed_context, _ = process_image_messages(context) if context else ([], [])
833
+ processed_messages, images = process_image_messages(messages)
834
+ prompt = _encode_messages_text(
835
+ processed_messages,
836
+ thinking_mode=thinking_mode,
837
+ context=processed_context if processed_context else None,
838
+ drop_thinking=drop_thinking,
839
+ add_default_bos_token=add_default_bos_token,
840
+ reasoning_effort=reasoning_effort,
841
+ )
842
+ if return_multi_modal_data:
843
+ return prompt, {"images": images}
844
+ return prompt
845
+
846
+
847
+ def load_cases(input_file: str) -> List[Dict[str, Any]]:
848
+ """Load one or more OpenAI-format conversation cases from JSON."""
849
+ with open(input_file) as file:
850
+ data = json.load(file)
851
+ if isinstance(data, dict):
852
+ data = [data]
853
+ elif data and isinstance(data[0], dict) and "role" in data[0]:
854
+ data = [{"messages": data}]
855
+
856
+ cases = []
857
+ for case in data:
858
+ messages = copy.deepcopy(case["messages"])
859
+ if "tools" in case:
860
+ if not messages:
861
+ raise ValueError("A case with tools must contain at least one message")
862
+ messages[0]["tools"] = case["tools"]
863
+ cases.append({
864
+ "messages": messages,
865
+ "context": case.get("context"),
866
+ "thinking_mode": case.get("thinking_mode"),
867
+ "reasoning_effort": case.get("reasoning_effort"),
868
+ })
869
+ return cases
870
+
871
+
872
+ def encode_case(
873
+ case: Dict[str, Any], thinking_mode: str
874
+ ) -> Tuple[str, List[Dict[str, Any]]]:
875
+ """Encode one JSON case and return its current-turn image records."""
876
+ prompt, media_data = encode_messages(
877
+ case["messages"],
878
+ thinking_mode=case.get("thinking_mode") or thinking_mode,
879
+ context=case.get("context"),
880
+ reasoning_effort=case.get("reasoning_effort"),
881
+ return_multi_modal_data=True,
882
+ )
883
+ return prompt, media_data["images"]
884
+
885
+
886
+ # ============================================================
887
+ # Parsing (Decoding model output)
888
+ # ============================================================
889
+
890
+ def parse_tool_calls(index: int, text: str) -> Tuple[int, Optional[str], List[Dict[str, str]]]:
891
+ """
892
+ Parse V4.1 DSML tool calls from text starting at the given index.
893
+
894
+ Returns:
895
+ Tuple of (new_index, last_stop_token, list_of_tool_call_dicts).
896
+ """
897
+ tool_calls: List[Dict[str, Any]] = []
898
+ stop_token = None
899
+ tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
900
+ tool_call_start_token = f"<{dsml_token}{tool_call_tag_name}"
901
+ tool_call_end_token = f"</{dsml_token}{tool_call_tag_name}"
902
+ tool_parameter_start_token = f"<{dsml_token}{tool_parameter_tag_name}"
903
+ tool_parameter_end_token = f"/{dsml_token}{tool_parameter_tag_name}"
904
+
905
+ while index < len(text):
906
+ index, _, stop_token = _read_until_stop(index, text, [tool_call_start_token, tool_calls_end_token])
907
+ if _ != ">\n":
908
+ raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
909
+
910
+ if stop_token == tool_calls_end_token:
911
+ break
912
+
913
+ if stop_token is None:
914
+ raise ValueError("Missing special token in tool calls")
915
+
916
+ index, tool_name_content, stop_token = _read_until_stop(index, text, [tool_parameter_start_token, tool_call_end_token])
917
+
918
+ p_tool_name = re.findall(r'^\s*name="(.*?)">\n$', tool_name_content, flags=re.DOTALL)
919
+ if len(p_tool_name) != 1:
920
+ raise ValueError(f"Tool name format error: '{tool_name_content}'")
921
+ tool_name = p_tool_name[0]
922
+
923
+ tool_args: Dict[str, Tuple[str, str]] = {}
924
+ while stop_token == tool_parameter_start_token:
925
+ index, param_content, stop_token = _read_until_stop(index, text, [tool_parameter_end_token])
926
+
927
+ param_kv = re.findall(r'^ name="(.*?)" string="(true|false)">(.*?)<$', param_content, flags=re.DOTALL)
928
+ if len(param_kv) != 1:
929
+ raise ValueError(f"Parameter format error: '{param_content}'")
930
+ param_name, string, param_value = param_kv[0]
931
+
932
+ if param_name in tool_args:
933
+ raise ValueError(f"Duplicate parameter name: '{param_name}'")
934
+ tool_args[param_name] = (param_value, string)
935
+
936
+ index, content, stop_token = _read_until_stop(index, text, [tool_parameter_start_token, tool_call_end_token])
937
+ if content != ">\n":
938
+ raise ValueError(f"Parameter format error: expected '>\\n' but got '{content}'")
939
+
940
+ tool_call = decode_dsml_to_arguments(tool_name=tool_name, tool_args=tool_args)
941
+ tool_calls.append(tool_call)
942
+
943
+ return index, stop_token, tool_calls
944
+
945
+
946
+ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[str, Any]:
947
+ """
948
+ Parse a model completion text into a structured assistant message (V4.1 format).
949
+
950
+ Returns:
951
+ Dict with keys: "role", "content", "reasoning_content", "tool_calls".
952
+ tool_calls are in OpenAI format.
953
+ """
954
+ summary_content, reasoning_content, tool_calls = "", "", []
955
+ index, stop_token = 0, None
956
+ tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
957
+
958
+ is_thinking = thinking_mode == "thinking"
959
+ is_tool_calling = False
960
+
961
+ if is_thinking:
962
+ index, content_delta, stop_token = _read_until_stop(index, text, [thinking_end_token, tool_calls_start_token])
963
+ reasoning_content = content_delta
964
+ assert stop_token == thinking_end_token, "Invalid thinking format: missing </think>"
965
+
966
+ index, content_delta, stop_token = _read_until_stop(index, text, [eos_token, tool_calls_start_token])
967
+ summary_content = content_delta
968
+ if stop_token == tool_calls_start_token:
969
+ is_tool_calling = True
970
+ else:
971
+ assert stop_token == eos_token, "Invalid format: missing EOS token"
972
+
973
+ if is_tool_calling:
974
+ index, stop_token, tool_calls = parse_tool_calls(index, text)
975
+
976
+ index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
977
+ assert not tool_ends_text, "Unexpected content after tool calls"
978
+
979
+ assert len(text) == index and stop_token in [eos_token, None], "Unexpected content at end"
980
+
981
+ for sp_token in [bos_token, eos_token, thinking_start_token, thinking_end_token, dsml_token]:
982
+ assert sp_token not in summary_content and sp_token not in reasoning_content, \
983
+ f"Unexpected special token '{sp_token}' in content"
984
+
985
+ return {
986
+ "role": "assistant",
987
+ "content": summary_content,
988
+ "reasoning_content": reasoning_content,
989
+ "tool_calls": tool_calls_to_openai_format(tool_calls)
990
+ }
encoding/test_encoding.py ADDED
@@ -0,0 +1,652 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for encoding.py (DeepSeek-V4.1 encoding).
3
+
4
+ Adapted from dsv41-master/deepseek_harmony/tests/test_deepseek_v41.py for the
5
+ self-contained dict-based API in this repo.
6
+ """
7
+
8
+ import copy
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import pytest
14
+
15
+ import encoding as enc
16
+ from encoding import (
17
+ IMAGE_PLACEHOLDER,
18
+ SYSTEM_SP_TOKEN,
19
+ encode_messages,
20
+ parse_message_from_completion_text,
21
+ render_message,
22
+ merge_tool_messages,
23
+ )
24
+
25
+
26
+ REASONING_EFFORT_TEMPLATE = (
27
+ SYSTEM_SP_TOKEN + "Reasoning Effort: {budget} "
28
+ "(range 1-100, the higher the value, the more thorough the reasoning)\n\n"
29
+ )
30
+
31
+ V41_TOOL_CALL_OUTPUT = (
32
+ ' reason </think>summary\n\n'
33
+ '<|DSML| calls>\n'
34
+ '<|DSML| invoke name="lookup">\n'
35
+ '<|DSML| parameter name="query" string="true">value'
36
+ '</|DSML| parameter>\n'
37
+ '<|DSML| parameter name="limit" string="false">2'
38
+ '</|DSML| parameter>\n'
39
+ '</|DSML| invoke>\n'
40
+ '</|DSML| calls><|end▁of▁sentence|>'
41
+ )
42
+
43
+
44
+ def make_tool() -> dict:
45
+ return {
46
+ "type": "function",
47
+ "function": {
48
+ "name": "lookup",
49
+ "description": "Look up a value",
50
+ "parameters": {
51
+ "type": "object",
52
+ "properties": {
53
+ "query": {"type": "string"},
54
+ "limit": {"type": "integer"},
55
+ },
56
+ },
57
+ },
58
+ }
59
+
60
+
61
+ def make_tool_call_messages() -> list:
62
+ return [
63
+ {"role": "user", "content": "question"},
64
+ {
65
+ "role": "assistant",
66
+ "reasoning_content": " reason ",
67
+ "content": "summary",
68
+ "tool_calls": [
69
+ {
70
+ "type": "function",
71
+ "function": {
72
+ "name": "lookup",
73
+ "arguments": '{"query":"value","limit":2}',
74
+ },
75
+ }
76
+ ],
77
+ },
78
+ ]
79
+
80
+
81
+ # ============================================================
82
+ # Vision
83
+ # ============================================================
84
+
85
+ def test_v41_renders_images() -> None:
86
+ prompt, media = encode_messages(
87
+ [
88
+ {
89
+ "role": "user",
90
+ "content": [
91
+ {"type": "text", "text": "inspect"},
92
+ {"type": "image_url", "image_url": {"url": "/unused/image.png"}},
93
+ ],
94
+ }
95
+ ],
96
+ thinking_mode="chat",
97
+ return_multi_modal_data=True,
98
+ )
99
+
100
+ assert prompt == (
101
+ '<|begin▁of▁sentence|><|User|>inspect\n\n'
102
+ f'{IMAGE_PLACEHOLDER}<|Assistant|></think>'
103
+ )
104
+ assert media == {"images": [{"type": "image", "url": "/unused/image.png"}]}
105
+
106
+
107
+ @pytest.mark.parametrize("role", ["user", "tool"])
108
+ def test_v41_rejects_image_placeholder_in_text(role: str) -> None:
109
+ with pytest.raises(ValueError):
110
+ encode_messages(
111
+ [{"role": role, "content": f"hi {IMAGE_PLACEHOLDER}"}],
112
+ thinking_mode="chat",
113
+ )
114
+
115
+
116
+ @pytest.mark.parametrize(
117
+ "input_format",
118
+ ["content_blocks", "tool_messages", "tool_content_blocks", "context"],
119
+ )
120
+ def test_v41_tool_result_images_follow_prompt_order(input_format: str) -> None:
121
+ history = [
122
+ {
123
+ "role": "user",
124
+ "content": [
125
+ {"type": "image_url", "image_url": {"url": "history.png"}},
126
+ ],
127
+ },
128
+ {
129
+ "role": "assistant",
130
+ "tool_calls": [
131
+ {
132
+ "id": name,
133
+ "type": "function",
134
+ "function": {"name": name, "arguments": "{}"},
135
+ }
136
+ for name in ("a", "b")
137
+ ],
138
+ },
139
+ ]
140
+ results = [
141
+ {
142
+ "type": "tool_result",
143
+ "tool_use_id": name,
144
+ "content": [
145
+ {"type": "text", "text": f"result {name}"},
146
+ {"type": "image_url", "image_url": {"url": f"{name}.png"}},
147
+ ],
148
+ }
149
+ for name in ("b", "a")
150
+ ]
151
+ if input_format in ("tool_messages", "tool_content_blocks"):
152
+ content_field = (
153
+ "content_blocks" if input_format == "tool_content_blocks" else "content"
154
+ )
155
+ messages = history + [
156
+ {
157
+ "role": "tool",
158
+ "tool_call_id": block["tool_use_id"],
159
+ content_field: block["content"],
160
+ }
161
+ for block in results
162
+ ]
163
+ context = []
164
+ elif input_format == "context":
165
+ messages = [{"role": "user", "content_blocks": results}]
166
+ context = history
167
+ else:
168
+ messages = history + [{"role": "user", "content_blocks": results}]
169
+ context = []
170
+ original = copy.deepcopy((messages, context))
171
+
172
+ prompt, media = encode_messages(
173
+ messages,
174
+ thinking_mode="chat",
175
+ context=context,
176
+ return_multi_modal_data=True,
177
+ )
178
+
179
+ assert prompt.endswith(
180
+ f"<|User|><tool_result>result a\n\n{IMAGE_PLACEHOLDER}</tool_result>\n\n"
181
+ f"<tool_result>result b\n\n{IMAGE_PLACEHOLDER}</tool_result><|Assistant|></think>"
182
+ )
183
+ expected_urls = ["a.png", "b.png"] if context else ["history.png", "a.png", "b.png"]
184
+ assert media == {"images": [{"type": "image", "url": url} for url in expected_urls]}
185
+ assert (messages, context) == original
186
+
187
+
188
+ # ============================================================
189
+ # Reasoning Effort
190
+ # ============================================================
191
+
192
+ @pytest.mark.parametrize(
193
+ ("effort", "budget"),
194
+ [
195
+ (None, 75),
196
+ ("low", 50),
197
+ ("high", 75),
198
+ ("max", 100),
199
+ (1, 1),
200
+ (42, 42),
201
+ (100, 100),
202
+ ],
203
+ )
204
+ def test_v41_maps_reasoning_effort_to_1_100_budget(
205
+ effort: Any,
206
+ budget: int,
207
+ ) -> None:
208
+ prompt = encode_messages(
209
+ [{"role": "user", "content": "question"}],
210
+ thinking_mode="thinking",
211
+ reasoning_effort=effort,
212
+ )
213
+
214
+ assert prompt == (
215
+ '<|begin▁of▁sentence|>'
216
+ f'{REASONING_EFFORT_TEMPLATE.format(budget=budget)}'
217
+ '<|User|>question<|Assistant|><think>'
218
+ )
219
+
220
+
221
+ def test_v41_only_adds_reasoning_effort_to_first_thinking_message() -> None:
222
+ messages = [
223
+ {"role": "system", "content": "system"},
224
+ {"role": "user", "content": "question"},
225
+ ]
226
+
227
+ later_message = render_message(
228
+ 1, messages, thinking_mode="thinking", reasoning_effort=100
229
+ )
230
+ chat_message = render_message(
231
+ 0, messages, thinking_mode="chat", reasoning_effort=100
232
+ )
233
+
234
+ assert "Reasoning Effort:" not in later_message
235
+ assert "Reasoning Effort:" not in chat_message
236
+
237
+
238
+ def test_v41_chat_mode_has_no_reasoning_effort_or_system_token() -> None:
239
+ prompt = encode_messages(
240
+ [{"role": "user", "content": "hello"}],
241
+ thinking_mode="chat",
242
+ reasoning_effort="max",
243
+ )
244
+ assert prompt == '<|begin▁of▁sentence|><|User|>hello<|Assistant|></think>'
245
+
246
+
247
+ @pytest.mark.parametrize("effort", [-1, 0, 101, "medium"])
248
+ def test_v41_rejects_out_of_range_or_unknown_reasoning_effort(
249
+ effort: Any,
250
+ ) -> None:
251
+ with pytest.raises(AssertionError, match=r"int within \[1,100\]"):
252
+ encode_messages(
253
+ [{"role": "user", "content": "question"}],
254
+ thinking_mode="thinking",
255
+ reasoning_effort=effort,
256
+ )
257
+
258
+
259
+ @pytest.mark.parametrize("effort", [True, False, 1.5])
260
+ def test_v41_rejects_non_string_non_integer_effort_types(effort: Any) -> None:
261
+ # bool is not `type(...) is int`; float is invalid too
262
+ with pytest.raises(AssertionError):
263
+ encode_messages(
264
+ [{"role": "user", "content": "question"}],
265
+ thinking_mode="thinking",
266
+ reasoning_effort=effort,
267
+ )
268
+
269
+
270
+ # ============================================================
271
+ # System token
272
+ # ============================================================
273
+
274
+ def test_v41_leading_system_message_uses_system_token() -> None:
275
+ prompt = encode_messages(
276
+ [
277
+ {"role": "system", "content": "You are a helpful assistant."},
278
+ {"role": "user", "content": "hello"},
279
+ ],
280
+ thinking_mode="chat",
281
+ )
282
+ assert prompt == (
283
+ '<|begin▁of▁sentence|><|System|>You are a helpful assistant.'
284
+ '<|User|>hello<|Assistant|></think>'
285
+ )
286
+
287
+
288
+ def test_v41_mid_conversation_system_message() -> None:
289
+ prompt = encode_messages(
290
+ [
291
+ {"role": "system", "content": "sys"},
292
+ {"role": "user", "content": "q1"},
293
+ {"role": "assistant", "content": "a1", "reasoning_content": "r1"},
294
+ {"role": "system", "content": "mid sys"},
295
+ ],
296
+ thinking_mode="thinking",
297
+ reasoning_effort=88,
298
+ )
299
+ # Mid-conversation system gets its own <|System|> token and triggers
300
+ # the assistant generation header afterwards.
301
+ assert prompt == (
302
+ '<|begin▁of▁sentence|>'
303
+ f'{REASONING_EFFORT_TEMPLATE.format(budget=88)}'
304
+ 'sys<|User|>q1<|Assistant|></think>a1<|end▁of▁sentence|>'
305
+ '<|System|>mid sys<|Assistant|><think>'
306
+ )
307
+
308
+
309
+ # ============================================================
310
+ # DSML tool tags
311
+ # ============================================================
312
+
313
+ def test_v41_tool_instructions_use_spaced_dsml_tags_in_chat_mode() -> None:
314
+ prompt = encode_messages(
315
+ [
316
+ {"role": "system", "content": "system", "tools": [make_tool()]},
317
+ {"role": "user", "content": "question"},
318
+ ],
319
+ thinking_mode="chat",
320
+ )
321
+
322
+ assert (
323
+ '<|DSML| calls>\n'
324
+ '<|DSML| invoke name="$TOOL_NAME">\n'
325
+ '<|DSML| parameter name="$PARAMETER_NAME" '
326
+ 'string="true|false">$PARAMETER_VALUE</|DSML| parameter>\n'
327
+ '...\n'
328
+ '</|DSML| invoke>'
329
+ ) in prompt
330
+ assert '<|DSML|tool_calls>' not in prompt
331
+ assert '<|DSML|invoke' not in prompt
332
+ assert '<|DSML|parameter' not in prompt
333
+
334
+
335
+ def test_v41_renders_spaced_dsml_with_v4_assistant_semantics() -> None:
336
+ messages = make_tool_call_messages()
337
+
338
+ prompt = render_message(1, messages, thinking_mode="thinking")
339
+
340
+ assert prompt == V41_TOOL_CALL_OUTPUT
341
+
342
+
343
+ def test_v41_parses_spaced_dsml_roundtrip() -> None:
344
+ messages = make_tool_call_messages()
345
+
346
+ parsed = parse_message_from_completion_text(
347
+ V41_TOOL_CALL_OUTPUT, thinking_mode="thinking"
348
+ )
349
+
350
+ assert parsed["role"] == "assistant"
351
+ assert parsed["reasoning_content"] == " reason "
352
+ assert parsed["content"] == "summary"
353
+ assert parsed["tool_calls"]
354
+ assert parsed["tool_calls"][0]["function"]["name"] == "lookup"
355
+ assert json.loads(parsed["tool_calls"][0]["function"]["arguments"]) == {
356
+ "query": "value",
357
+ "limit": 2,
358
+ }
359
+
360
+ # Re-encoding the parsed message reproduces the original completion text
361
+ assert encode_messages(
362
+ [parsed],
363
+ thinking_mode="thinking",
364
+ context=messages[:1],
365
+ ) == V41_TOOL_CALL_OUTPUT
366
+
367
+
368
+ def test_v41_parse_rejects_unspaced_v4_dsml() -> None:
369
+ v4_output = V41_TOOL_CALL_OUTPUT.replace("|DSML| calls", "|DSML|tool_calls") \
370
+ .replace("|DSML| invoke", "|DSML|invoke") \
371
+ .replace("|DSML| parameter", "|DSML|parameter")
372
+ with pytest.raises(AssertionError):
373
+ parse_message_from_completion_text(v4_output, thinking_mode="thinking")
374
+
375
+
376
+ # ============================================================
377
+ # Tool namespaces
378
+ # ============================================================
379
+
380
+ @pytest.mark.parametrize("location", ["tool", "function"])
381
+ @pytest.mark.parametrize("namespace", ["search", {"name": "search", "description": "Search tools."}])
382
+ def test_v41_renders_namespaced_tool_schemas(location: str, namespace: Any) -> None:
383
+ tool = make_tool()
384
+ target = tool if location == "tool" else tool["function"]
385
+ target["namespace"] = namespace
386
+ original = copy.deepcopy(tool)
387
+
388
+ prompt = encode_messages(
389
+ [{"role": "system", "content": "system", "tools": [tool]}],
390
+ thinking_mode="chat",
391
+ )
392
+
393
+ schema = dict(make_tool()["function"], name="search::lookup")
394
+ if isinstance(namespace, dict):
395
+ schema["description"] = "Search tools.\nLook up a value"
396
+ assert json.dumps(schema) in prompt
397
+ assert '"namespace":' not in prompt
398
+ assert tool == original
399
+
400
+
401
+ @pytest.mark.parametrize("thinking_mode", ["chat", "thinking"])
402
+ @pytest.mark.parametrize("location", ["tool", "function", "qualified_name"])
403
+ def test_v41_namespaced_tool_calls_roundtrip(thinking_mode: str, location: str) -> None:
404
+ messages = make_tool_call_messages()
405
+ call = messages[1]["tool_calls"][0]
406
+ if location == "qualified_name":
407
+ call["function"]["name"] = "search::lookup"
408
+ else:
409
+ target = call if location == "tool" else call["function"]
410
+ target["namespace"] = "search"
411
+ original = copy.deepcopy(messages)
412
+
413
+ expected = V41_TOOL_CALL_OUTPUT.replace('name="lookup"', 'name="search::lookup"')
414
+ if thinking_mode == "chat":
415
+ expected = expected.split("</think>", 1)[1]
416
+ assert render_message(1, messages, thinking_mode=thinking_mode) == expected
417
+
418
+ parsed = parse_message_from_completion_text(expected, thinking_mode=thinking_mode)
419
+ assert parsed["tool_calls"] == [{
420
+ "type": "function",
421
+ "namespace": "search",
422
+ "function": {
423
+ "name": "lookup",
424
+ "arguments": '{"query": "value", "limit": 2}',
425
+ },
426
+ }]
427
+ assert encode_messages(
428
+ [parsed], thinking_mode=thinking_mode, context=messages[:1]
429
+ ) == expected
430
+ assert messages == original
431
+
432
+
433
+ def test_v41_keeps_same_named_tools_in_separate_namespaces() -> None:
434
+ tools, calls = [], []
435
+ for namespace in (None, "search", "files"):
436
+ tool = make_tool()
437
+ call = {
438
+ "type": "function",
439
+ "function": {"name": "lookup", "arguments": '{"query":"value"}'},
440
+ }
441
+ if namespace is not None:
442
+ tool["namespace"] = {"name": namespace}
443
+ call["namespace"] = namespace
444
+ tools.append(tool)
445
+ calls.append(call)
446
+
447
+ messages = [
448
+ {"role": "system", "content": "system", "tools": tools},
449
+ {"role": "user", "content": "question"},
450
+ {"role": "assistant", "content": "summary", "tool_calls": calls},
451
+ ]
452
+ prompt = encode_messages(messages, thinking_mode="chat")
453
+ for name in ("lookup", "search::lookup", "files::lookup"):
454
+ assert f'"name": "{name}"' in prompt
455
+ assert f'<|DSML| invoke name="{name}">' in prompt
456
+
457
+ completion = render_message(2, messages, thinking_mode="chat")
458
+ parsed = parse_message_from_completion_text(completion, thinking_mode="chat")
459
+ assert "namespace" not in parsed["tool_calls"][0]
460
+ assert [call.get("namespace") for call in parsed["tool_calls"]] == [None, "search", "files"]
461
+ assert all(call["function"]["name"] == "lookup" for call in parsed["tool_calls"])
462
+
463
+
464
+ def test_v41_does_not_duplicate_a_qualified_namespace() -> None:
465
+ tool = make_tool()
466
+ tool["function"]["name"] = "search::lookup"
467
+ tool["namespace"] = {"name": "search", "description": "Search tools."}
468
+ schema = enc.tools_from_openai_format([tool])[0]
469
+ assert schema["name"] == "search::lookup"
470
+ assert schema["description"] == "Search tools.\nLook up a value"
471
+
472
+ messages = make_tool_call_messages()
473
+ call = messages[1]["tool_calls"][0]
474
+ call["function"]["name"] = "search::lookup"
475
+ call["namespace"] = "search"
476
+ assert render_message(1, messages, thinking_mode="thinking") == (
477
+ V41_TOOL_CALL_OUTPUT.replace('name="lookup"', 'name="search::lookup"')
478
+ )
479
+
480
+
481
+ @pytest.mark.parametrize(
482
+ ("name", "namespace", "error"),
483
+ [
484
+ ("search::lookup", "files", "Conflicting tool namespaces"),
485
+ ("search::nested::lookup", None, "Tool name must not contain"),
486
+ ("lookup", "search::nested", "Tool namespace must not contain"),
487
+ ],
488
+ )
489
+ def test_v41_rejects_ambiguous_tool_namespaces(name: str, namespace: Any, error: str) -> None:
490
+ tool = make_tool()
491
+ tool["function"]["name"] = name
492
+ tool["namespace"] = namespace
493
+ with pytest.raises(AssertionError, match=error):
494
+ enc.tools_from_openai_format([tool])
495
+
496
+ messages = make_tool_call_messages()
497
+ call = messages[1]["tool_calls"][0]
498
+ call["function"]["name"] = name
499
+ call["namespace"] = namespace
500
+ with pytest.raises(AssertionError, match=error):
501
+ render_message(1, messages, thinking_mode="thinking")
502
+
503
+
504
+ # ============================================================
505
+ # Multi-turn flow
506
+ # ============================================================
507
+
508
+ def test_v41_drop_thinking_without_tools() -> None:
509
+ messages = [
510
+ {"role": "user", "content": "q1"},
511
+ {"role": "assistant", "content": "a1", "reasoning_content": "r1"},
512
+ {"role": "user", "content": "q2"},
513
+ {
514
+ "role": "assistant",
515
+ "reasoning_content": "r2",
516
+ "tool_calls": [
517
+ {
518
+ "id": "c1",
519
+ "type": "function",
520
+ "function": {"name": "lookup", "arguments": "{}"},
521
+ }
522
+ ],
523
+ },
524
+ {"role": "tool", "tool_call_id": "c1", "content": "value"},
525
+ ]
526
+ prompt = encode_messages(
527
+ messages,
528
+ thinking_mode="thinking",
529
+ drop_thinking=True,
530
+ )
531
+ # Drop completed-turn reasoning, but keep the current turn across tool results.
532
+ assert "<|User|>q1<|Assistant|></think>a1<|end▁of▁sentence|>" in prompt
533
+ assert "r1" not in prompt
534
+ assert "<think>r2</think>" in prompt
535
+ assert prompt.endswith("<tool_result>value</tool_result><|Assistant|><think>")
536
+
537
+ next_turn = encode_messages(
538
+ messages + [{"role": "user", "content": "q3"}],
539
+ thinking_mode="thinking",
540
+ )
541
+ assert "r2" not in next_turn
542
+ assert next_turn.endswith("q3<|Assistant|><think>")
543
+
544
+
545
+ # ============================================================
546
+ # Preprocessing
547
+ # ============================================================
548
+
549
+ def test_merge_tool_messages_creates_tool_result_blocks() -> None:
550
+ merged = merge_tool_messages([
551
+ {"role": "assistant", "content": "", "tool_calls": []},
552
+ {"role": "tool", "tool_call_id": "a", "content": "r1"},
553
+ {"role": "tool", "tool_call_id": "b", "content": "r2"},
554
+ ])
555
+ assert len(merged) == 2
556
+ assert merged[1]["role"] == "user"
557
+ assert [b["type"] for b in merged[1]["content_blocks"]] == ["tool_result", "tool_result"]
558
+
559
+
560
+ def test_v41_task_sp_token() -> None:
561
+ prompt = encode_messages(
562
+ [{"role": "user", "content": "classify me", "task": "query"}],
563
+ thinking_mode="chat",
564
+ )
565
+ assert prompt.endswith("classify me<|query|>")
566
+ assert "<|Assistant|>" not in prompt
567
+
568
+
569
+ # ============================================================
570
+ # Golden fixtures from encoding/tests
571
+ # ============================================================
572
+
573
+ ENCODING_DIR = Path(__file__).resolve().parent
574
+ ENCODING_FIXTURES_DIR = ENCODING_DIR / "tests"
575
+ INFERENCE_EXAMPLES_DIR = ENCODING_DIR.parent / "inference" / "examples"
576
+
577
+ FIXTURE_CASE_IDS = sorted(
578
+ int(p.stem.split("_")[-1])
579
+ for p in ENCODING_FIXTURES_DIR.glob("test_input_*.json")
580
+ )
581
+
582
+
583
+ @pytest.mark.parametrize("case_id", FIXTURE_CASE_IDS)
584
+ def test_examples_encoding_golden_outputs(case_id: int) -> None:
585
+ """Each tests/encoding input must encode to its checked-in golden output."""
586
+ input_file = ENCODING_FIXTURES_DIR / f"test_input_{case_id}.json"
587
+ output_file = ENCODING_FIXTURES_DIR / f"test_output_{case_id}.txt"
588
+ assert output_file.exists(), f"missing golden output: {output_file.name} (run tests/encoding/regen_outputs.py)"
589
+
590
+ case = enc.load_cases(str(input_file))[0]
591
+ prompt, _ = enc.encode_case(case, thinking_mode="chat")
592
+
593
+ assert prompt == output_file.read_text(), (
594
+ f"{output_file.name} is stale; regenerate with tests/encoding/regen_outputs.py"
595
+ )
596
+
597
+
598
+ def test_examples_v41_output_uses_v41_format_markers() -> None:
599
+ """Sanity-check the V4.1 goldens actually exercise V4.1-specific format."""
600
+ # case 1: tool calls with spaced DSML tags
601
+ out1 = (ENCODING_FIXTURES_DIR / "test_output_1.txt").read_text()
602
+ assert '<|DSML| calls>' in out1 and '<|DSML| invoke name="get_weather">' in out1
603
+ assert '<|DSML|tool_calls>' not in out1
604
+
605
+ # case 5: numeric reasoning effort behind the system token
606
+ out5 = (ENCODING_FIXTURES_DIR / "test_output_5.txt").read_text()
607
+ assert out5.startswith(
608
+ '<|begin▁of▁sentence|>' + REASONING_EFFORT_TEMPLATE.format(budget=100)
609
+ )
610
+ assert out5.count(IMAGE_PLACEHOLDER) == 2
611
+
612
+
613
+ def test_examples_vl_txt_and_json_encode_identically() -> None:
614
+ """The TXT (last block of example.txt) and JSON vision examples must encode identically."""
615
+ txt = (INFERENCE_EXAMPLES_DIR / "example.txt").read_text().rstrip("\n").split("\n\n")[-1]
616
+ messages = [{"role": "user", "content": enc.parse_tagged_text(txt)}]
617
+ p1, m1 = encode_messages(messages, thinking_mode="chat", return_multi_modal_data=True)
618
+
619
+ case = enc.load_cases(str(INFERENCE_EXAMPLES_DIR / "example_harmony.json"))[0]
620
+ p2, m2 = enc.encode_case(case, thinking_mode="chat")
621
+
622
+ assert p1 == p2
623
+ assert m1["images"] == m2
624
+ assert len(m2) == 2
625
+
626
+
627
+ def test_examples_harmony_cases_encode() -> None:
628
+ """All example_harmony.json cases encode without error."""
629
+ cases = enc.load_cases(str(INFERENCE_EXAMPLES_DIR / "example_harmony.json"))
630
+ assert len(cases) == 4
631
+
632
+ # case 1 (vision) is covered by test_examples_vl_txt_and_json_encode_identically
633
+
634
+ # cases are pure OpenAI format: mode/effort are passed at call time
635
+ prompt = encode_messages(
636
+ cases[1]["messages"], thinking_mode="thinking", reasoning_effort=75
637
+ )
638
+ assert REASONING_EFFORT_TEMPLATE.format(budget=75) in prompt
639
+
640
+ # case 3: tools with spaced DSML tags
641
+ prompt, _ = enc.encode_case(cases[2], thinking_mode="chat")
642
+ assert '<|DSML| calls>' in prompt
643
+
644
+ # case 4: mid-conversation system message triggers assistant header
645
+ prompt, _ = enc.encode_case(cases[3], thinking_mode="chat")
646
+ assert '<|System|>Mid-conversation instruction update' in prompt
647
+ assert prompt.endswith('<|Assistant|></think>')
648
+
649
+
650
+ if __name__ == "__main__":
651
+ import sys
652
+ sys.exit(pytest.main([__file__, "-v"]))
encoding/tests/test_input_1.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "thinking_mode": "thinking",
3
+ "tools": [
4
+ {
5
+ "type": "function",
6
+ "function": {
7
+ "name": "get_weather",
8
+ "description": "Get the weather for a specific location",
9
+ "parameters": {
10
+ "type": "object",
11
+ "properties": {
12
+ "location": {
13
+ "type": "string",
14
+ "description": "The city name"
15
+ },
16
+ "unit": {
17
+ "type": "string",
18
+ "enum": ["celsius", "fahrenheit"],
19
+ "description": "Temperature unit"
20
+ }
21
+ },
22
+ "required": ["location"]
23
+ }
24
+ }
25
+ },
26
+ {
27
+ "type": "function",
28
+ "function": {
29
+ "name": "search",
30
+ "description": "Search the web for information",
31
+ "parameters": {
32
+ "type": "object",
33
+ "properties": {
34
+ "query": {
35
+ "type": "string",
36
+ "description": "Search query"
37
+ },
38
+ "num_results": {
39
+ "type": "integer",
40
+ "description": "Number of results to return"
41
+ }
42
+ },
43
+ "required": ["query"]
44
+ }
45
+ }
46
+ }
47
+ ],
48
+ "messages": [
49
+ {
50
+ "role": "system",
51
+ "content": "You are a helpful assistant."
52
+ },
53
+ {
54
+ "role": "user",
55
+ "content": "What's the weather like in Beijing?"
56
+ },
57
+ {
58
+ "role": "assistant",
59
+ "reasoning_content": "The user wants the weather in Beijing. I should call get_weather.",
60
+ "content": "",
61
+ "tool_calls": [
62
+ {
63
+ "type": "function",
64
+ "function": {
65
+ "name": "get_weather",
66
+ "arguments": "{\"location\": \"Beijing\", \"unit\": \"celsius\"}"
67
+ }
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "role": "tool",
73
+ "tool_call_id": "call_0",
74
+ "content": "{\"temperature\": 22, \"condition\": \"sunny\", \"humidity\": 45}"
75
+ },
76
+ {
77
+ "role": "assistant",
78
+ "reasoning_content": "Got the weather data. Let me format a nice response.",
79
+ "content": "The weather in Beijing is currently sunny with a temperature of 22\u00b0C and 45% humidity."
80
+ }
81
+ ]
82
+ }
encoding/tests/test_input_2.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "system",
4
+ "content": "You are a helpful assistant."
5
+ },
6
+ {
7
+ "role": "user",
8
+ "content": "Hello"
9
+ },
10
+ {
11
+ "role": "assistant",
12
+ "reasoning_content": "The user said hello, I should greet back.",
13
+ "content": "Hi there! How can I help you?"
14
+ },
15
+ {
16
+ "role": "user",
17
+ "content": "What is the capital of France?"
18
+ },
19
+ {
20
+ "role": "assistant",
21
+ "reasoning_content": "The user asks about the capital of France. It is Paris.",
22
+ "content": "The capital of France is Paris."
23
+ }
24
+ ]
encoding/tests/test_input_3.json ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "system",
4
+ "content": "该助手为DeepSeek,由深度求索公司创造。"
5
+ },
6
+ {
7
+ "role": "latest_reminder",
8
+ "content": "2026-02-21,星期六,广州,App,中文"
9
+ },
10
+ {
11
+ "role": "system",
12
+ "content": "CITATION FORMAT: 【{cursor_id}†L{start_line_id}(-L{end_line_id})?】",
13
+ "tools": [
14
+ {
15
+ "type": "function",
16
+ "function": {
17
+ "name": "search",
18
+ "description": "Web search. Split multiple queries with '||'.",
19
+ "parameters": {
20
+ "type": "object",
21
+ "properties": {
22
+ "queries": {
23
+ "type": "string",
24
+ "description": "query1||query2"
25
+ }
26
+ },
27
+ "required": ["queries"],
28
+ "additionalProperties": false
29
+ }
30
+ }
31
+ },
32
+ {
33
+ "type": "function",
34
+ "function": {
35
+ "name": "open",
36
+ "description": "Batch open IDs (format 【{id}†...】) or URLs.",
37
+ "parameters": {
38
+ "type": "object",
39
+ "properties": {
40
+ "open_list": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "id": {
46
+ "description": "ID or URL",
47
+ "anyOf": [{"type": "integer"}, {"type": "string"}],
48
+ "default": -1
49
+ },
50
+ "loc": {"type": "integer", "description": "Start line", "default": -1},
51
+ "num_lines": {"type": "integer", "description": "", "default": -1}
52
+ },
53
+ "additionalProperties": false
54
+ },
55
+ "description": ""
56
+ }
57
+ },
58
+ "required": ["open_list"],
59
+ "additionalProperties": false
60
+ }
61
+ }
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ "role": "user",
67
+ "content": "小柴胡冲剂和布洛芬能一起吃吗?"
68
+ },
69
+ {
70
+ "role": "assistant",
71
+ "content": "",
72
+ "reasoning_content": "用户想知道小柴胡冲剂和布洛芬能否一起服用。",
73
+ "tool_calls": [
74
+ {
75
+ "type": "function",
76
+ "function": {
77
+ "name": "search",
78
+ "arguments": "{\"queries\": \"小柴胡冲剂 布洛芬 相互作用 一起吃\"}"
79
+ }
80
+ }
81
+ ]
82
+ },
83
+ {
84
+ "role": "tool",
85
+ "content": "[0]"
86
+ },
87
+ {
88
+ "role": "assistant",
89
+ "content": "请及时就医。",
90
+ "reasoning_content": "现在开始组织回答。",
91
+ "tool_calls": []
92
+ }
93
+ ]
encoding/tests/test_input_4.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "system",
4
+ "content": "该助手为DeepSeek-V3,由深度求索公司创造。\n今天是2025年10月17日,星期五。"
5
+ },
6
+ {
7
+ "role": "latest_reminder",
8
+ "content": "2024-11-15,上海市,App,中文"
9
+ },
10
+ {
11
+ "role": "user",
12
+ "content": "热海大滚锅是世界著名温泉吗"
13
+ },
14
+ {
15
+ "role": "assistant",
16
+ "content": "热海大滚锅在中国乃至全球的地热奇观中占有重要地位,但“世界著名”的称号更侧重于它作为独特的地质现象和旅游景点。",
17
+ "mask": 1
18
+ },
19
+ {
20
+ "role": "user",
21
+ "content": "世界著名温泉有哪些",
22
+ "task": "action"
23
+ },
24
+ {
25
+ "role": "assistant",
26
+ "content": "Search"
27
+ }
28
+ ]
encoding/tests/test_input_5.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "thinking_mode": "thinking",
3
+ "reasoning_effort": "max",
4
+ "messages": [
5
+ {
6
+ "role": "system",
7
+ "content": "You are a helpful vision assistant."
8
+ },
9
+ {
10
+ "role": "user",
11
+ "content": [
12
+ {
13
+ "type": "text",
14
+ "text": "请按“第一张、第二张”的顺序回答:第一张图"
15
+ },
16
+ {
17
+ "type": "image_url",
18
+ "image_url": {
19
+ "url": "examples/images/carrots.jpeg"
20
+ }
21
+ },
22
+ {
23
+ "type": "text",
24
+ "text": "和第二张图"
25
+ },
26
+ {
27
+ "type": "image_url",
28
+ "image_url": {
29
+ "url": "examples/images/corn.jpeg"
30
+ }
31
+ },
32
+ {
33
+ "type": "text",
34
+ "text": "中分别是什么食材?它们通常食用的部位分别是什么?"
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
encoding/tests/test_output_1.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <|begin▁of▁sentence|><|System|>Reasoning Effort: 75 (range 1-100, the higher the value, the more thorough the reasoning)
2
+
3
+ You are a helpful assistant.
4
+
5
+ ## Tools
6
+
7
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<|DSML| calls>" block like the following:
8
+
9
+ <|DSML| calls>
10
+ <|DSML| invoke name="$TOOL_NAME">
11
+ <|DSML| parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</|DSML| parameter>
12
+ ...
13
+ </|DSML| invoke>
14
+ <|DSML| invoke name="$TOOL_NAME2">
15
+ ...
16
+ </|DSML| invoke>
17
+ </|DSML| calls>
18
+
19
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
20
+
21
+ If thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
22
+
23
+ Otherwise, output directly after </think> with tool calls or final response.
24
+
25
+ ### Available Tool Schemas
26
+
27
+ {"name": "get_weather", "description": "Get the weather for a specific location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city name"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit"}}, "required": ["location"]}}
28
+ {"name": "search", "description": "Search the web for information", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}, "num_results": {"type": "integer", "description": "Number of results to return"}}, "required": ["query"]}}
29
+
30
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
31
+ <|User|>What's the weather like in Beijing?<|Assistant|><think>The user wants the weather in Beijing. I should call get_weather.</think>
32
+
33
+ <|DSML| calls>
34
+ <|DSML| invoke name="get_weather">
35
+ <|DSML| parameter name="location" string="true">Beijing</|DSML| parameter>
36
+ <|DSML| parameter name="unit" string="true">celsius</|DSML| parameter>
37
+ </|DSML| invoke>
38
+ </|DSML| calls><|end▁of▁sentence|><|User|><tool_result>{"temperature": 22, "condition": "sunny", "humidity": 45}</tool_result><|Assistant|><think>Got the weather data. Let me format a nice response.</think>The weather in Beijing is currently sunny with a temperature of 22°C and 45% humidity.<|end▁of▁sentence|>
encoding/tests/test_output_2.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ <|begin▁of▁sentence|><|System|>You are a helpful assistant.<|User|>Hello<|Assistant|></think>Hi there! How can I help you?<|end▁of▁sentence|><|User|>What is the capital of France?<|Assistant|></think>The capital of France is Paris.<|end▁of▁sentence|>
encoding/tests/test_output_3.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <|begin▁of▁sentence|><|System|>该助手为DeepSeek,由深度求索公司创造。<|latest_reminder|>2026-02-21,星期六,广州,App,中文<|System|>CITATION FORMAT: 【{cursor_id}†L{start_line_id}(-L{end_line_id})?】
2
+
3
+ ## Tools
4
+
5
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<|DSML| calls>" block like the following:
6
+
7
+ <|DSML| calls>
8
+ <|DSML| invoke name="$TOOL_NAME">
9
+ <|DSML| parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</|DSML| parameter>
10
+ ...
11
+ </|DSML| invoke>
12
+ <|DSML| invoke name="$TOOL_NAME2">
13
+ ...
14
+ </|DSML| invoke>
15
+ </|DSML| calls>
16
+
17
+ String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
18
+
19
+ If thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
20
+
21
+ Otherwise, output directly after </think> with tool calls or final response.
22
+
23
+ ### Available Tool Schemas
24
+
25
+ {"name": "search", "description": "Web search. Split multiple queries with '||'.", "parameters": {"type": "object", "properties": {"queries": {"type": "string", "description": "query1||query2"}}, "required": ["queries"], "additionalProperties": false}}
26
+ {"name": "open", "description": "Batch open IDs (format 【{id}†...】) or URLs.", "parameters": {"type": "object", "properties": {"open_list": {"type": "array", "items": {"type": "object", "properties": {"id": {"description": "ID or URL", "anyOf": [{"type": "integer"}, {"type": "string"}], "default": -1}, "loc": {"type": "integer", "description": "Start line", "default": -1}, "num_lines": {"type": "integer", "description": "", "default": -1}}, "additionalProperties": false}, "description": ""}}, "required": ["open_list"], "additionalProperties": false}}
27
+
28
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
29
+ <|User|>小柴胡冲剂和布洛芬能一起吃吗?<|Assistant|></think>
30
+
31
+ <|DSML| calls>
32
+ <|DSML| invoke name="search">
33
+ <|DSML| parameter name="queries" string="true">小柴胡冲剂 布洛芬 相互作用 一起吃</|DSML| parameter>
34
+ </|DSML| invoke>
35
+ </|DSML| calls><|end▁of▁sentence|><|User|><tool_result>[0]</tool_result><|Assistant|></think>请及时就医。<|end▁of▁sentence|>
encoding/tests/test_output_4.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <|begin▁of▁sentence|><|System|>该助手为DeepSeek-V3,由深度求索公司创造。
2
+ 今天是2025年10月17日,星期五。<|latest_reminder|>2024-11-15,上海市,App,中文<|User|>热海大滚锅是世界著名温泉吗<|Assistant|></think>热海大滚锅在中国乃至全球的地热奇观中占有重要地位,但“世界著名”的称号更侧重于它作为独特的地质现象和旅游景点。<|end▁of▁sentence|><|User|>世界著名温泉有哪些<|Assistant|></think><|action|>Search<|end▁of▁sentence|>
encoding/tests/test_output_5.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <|begin▁of▁sentence|><|System|>Reasoning Effort: 100 (range 1-100, the higher the value, the more thorough the reasoning)
2
+
3
+ You are a helpful vision assistant.<|User|>请按“第一张、第二张”的顺序回答:第一张图
4
+
5
+ <|deepseek_image|>
6
+
7
+ 和第二张图
8
+
9
+ <|deepseek_image|>
10
+
11
+ 中分别是什么食材?它们通常食用的部位分别是什么?<|Assistant|><think>
exl3_k_map.json ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "codebook": "mul1",
3
+ "mul1_multiplier": 2212286765,
4
+ "mul1_marker_i32": -2082680531,
5
+ "routed": {
6
+ "0": 4,
7
+ "1": 4,
8
+ "2": 4,
9
+ "3": 4,
10
+ "4": 4,
11
+ "5": 4,
12
+ "6": 4,
13
+ "7": 4,
14
+ "8": 4,
15
+ "9": 4,
16
+ "10": 3,
17
+ "11": 3,
18
+ "12": 3,
19
+ "13": 3,
20
+ "14": 3,
21
+ "15": 3,
22
+ "16": 3,
23
+ "17": 3,
24
+ "18": 3,
25
+ "19": 3,
26
+ "20": 3,
27
+ "21": 3,
28
+ "22": 3,
29
+ "23": 3,
30
+ "24": 3,
31
+ "25": 3,
32
+ "26": 3,
33
+ "27": 3,
34
+ "28": 3,
35
+ "29": 3,
36
+ "30": 4,
37
+ "31": 4,
38
+ "32": 4,
39
+ "33": 4,
40
+ "34": 4,
41
+ "35": 4,
42
+ "36": 4,
43
+ "37": 4,
44
+ "38": 4,
45
+ "39": 4
46
+ },
47
+ "shared": {
48
+ "0": 6,
49
+ "1": 6,
50
+ "2": 6,
51
+ "3": 6,
52
+ "4": 6,
53
+ "5": 6,
54
+ "6": 6,
55
+ "7": 6,
56
+ "8": 6,
57
+ "9": 6,
58
+ "10": 6,
59
+ "11": 6,
60
+ "12": 6,
61
+ "13": 6,
62
+ "14": 6,
63
+ "15": 6,
64
+ "16": 6,
65
+ "17": 6,
66
+ "18": 6,
67
+ "19": 6,
68
+ "20": 6,
69
+ "21": 6,
70
+ "22": 6,
71
+ "23": 6,
72
+ "24": 6,
73
+ "25": 6,
74
+ "26": 6,
75
+ "27": 6,
76
+ "28": 6,
77
+ "29": 6,
78
+ "30": 6,
79
+ "31": 6,
80
+ "32": 6,
81
+ "33": 6,
82
+ "34": 6,
83
+ "35": 6,
84
+ "36": 6,
85
+ "37": 6,
86
+ "38": 6,
87
+ "39": 6
88
+ },
89
+ "shared_overrides": {},
90
+ "engram": {
91
+ "1": 6,
92
+ "14": 6
93
+ },
94
+ "mtp_bits": 6,
95
+ "head_bits": 6,
96
+ "attn_default": 6,
97
+ "indexer_wk_bits": 8,
98
+ "num_hidden_layers": 40
99
+ }
model-00001-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba5ccb4d39882d08800e856150bce42b23584f3d9943520ae4cff5647d5a3045
3
+ size 1323827293
model-00002-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:012504ba47677b9bb8727bb89cb8c6327a066ef10d2ade179f7ffd5c8151ff84
3
+ size 7060206910
model-00003-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f9d5228106bd5c8064d672f5ae24cce6005be8c2cf33592ef0478655c9e9b80
3
+ size 6942014187
model-00004-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cae1bf802a2e10c59e1f3e057b58f183a0f64e6bf4a1c76107168a9cbb9d532
3
+ size 6950309596
model-00005-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b583b45c6a5848dc33a7219d2c34149663779059b11b1bba5a9e76951b333af7
3
+ size 6942014187
model-00006-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4d2e5290f5253262adb8b83b91f37406f24d0a1ce14c7c6844734aa2cbe064a
3
+ size 6942014187
model-00007-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:65a639c07a74c59da458298adc99bca87036f8316f9c09ac409e2c9b68d414ba
3
+ size 6942014187
model-00008-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90f9448977e45f2db5859ca7c9d33ea34a6cdaffc4c28c0d8bb030938db4d614
3
+ size 6942014187
model-00009-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06cd3e7672a90c4edfb9ff98b77d11261e39a21558af314f2b235ea5b90b7518
3
+ size 6942014187
model-00010-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:286c1c0597f29d154391d9f8252cabc560a89be68b50c1f227d0b078200336b2
3
+ size 6950309596
model-00011-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b644d168923b319816185fc99b77478db7698067e5e8c67fae619f56a6b18405
3
+ size 6942014187
model-00012-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8e278e5d6a44c81d6fe16c9231aa2cb3823cb8d26e55bdb491345f4e0797f28
3
+ size 5243325245
model-00013-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39e25072f8f8de381b3176cb56eb0ec7caa8ab70e85956b57d7f9452291cd386
3
+ size 5243325245
model-00014-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3e6e26fad54c7cbc90fbda7209a2f389ff73e58160fb60326bcaa5635e52d811
3
+ size 5243325245
model-00015-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e34e397fb634b06a8558643d6b621a2b560b68c0766aac9c0252451ea3daffe2
3
+ size 5361517974
model-00016-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db9782ab6346e2dbcf259b79289e6dbc6b4fe183c4936bc1c1c892e2604fd6c7
3
+ size 5251620673
model-00017-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:711c747176c8494d0cef290e98de5026658bd79a3ef51ed4d43ce403acbfa481
3
+ size 5243325245
model-00018-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:111ce8d8496d6265098337f5b5ff79e71a31ba340eb2b682d0042dbca4dc954a
3
+ size 5243325245
model-00019-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6feedcd839b5593b275761107593ad9f9a0244098dffed66ae6f88ead8844c0d
3
+ size 5243325245
model-00020-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bf22fe666d473899b7d6f7bfa1709ad3cffe93f69d39a3da272eac854b39a882
3
+ size 5243325245
model-00021-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:afef0df9f451e9392cbf4704c9f8ee9183695e543b669d811b6eca88c21a7fe2
3
+ size 5243325245
model-00022-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:987c51d505f4b49442481ad8596256d846067e8c1fa8edb6c90bd6fc3f85d454
3
+ size 5249642888
model-00023-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:38cfb33a3a47483a1b1f7b3eebd22ffc9087437d45c8561062a72a8798fa48ef
3
+ size 5243325245
model-00024-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aec09849c4916f7254129324bb6aa73eb3685448709534f66fbb31009e0edaf1
3
+ size 5243325245
model-00025-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e0730da79761933690a243e2dd7dd8ecada43c95d9ec96fcc3a520b9f3194a2
3
+ size 5243325245
model-00026-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9a15b99de6190d10099c62383e283c32ef42fdab8ec37ebf355448669eeb027
3
+ size 5247596381
model-00027-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c673fa72f6aca977106267afb63f218f241f49a57eb833311ec8affe16009560
3
+ size 5243325245
model-00028-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e54fe28e42bd8eec92fa07ac0e4be0850149b20b74e15d7af37d5314a9842deb
3
+ size 5243325245
model-00029-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f69616591acdc45b7760db5ef9fab39dfb66558a908778aab230bd8c0d9faa3d
3
+ size 5243325245
model-00030-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ac4862535de5828c521e7e5e13906f993395940693cb66fbd323ce9360cd1917
3
+ size 5247596381
model-00031-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0beae804fe362965413ba7d26c9c902cf44dd77824838239878332c95170dc7b
3
+ size 5243325245
model-00032-of-00044.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48df9384d595f94b0093b9182b63b4c48216c3c1e486f7aa43a894e93521785d
3
+ size 6942018869