jondurbin commited on
Commit
fb9bda4
·
verified ·
1 Parent(s): 364ae84

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ 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
+ intelligence_score_vs_output_tokens.png filter=lfs diff=lfs merge=lfs -text
LICENSE.DeepSeek 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.
README.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: transformers
4
+ base_model:
5
+ - deepseek-ai/DeepSeek-R1-0528
6
+ - deepseek-ai/DeepSeek-R1
7
+ - deepseek-ai/DeepSeek-V3-0324
8
+ pipeline_tag: text-generation
9
+ ---
10
+ # DeepSeek-TNG-R1T2-Chimera
11
+
12
+ <div align="center">
13
+ <img src="https://354918363417-runtime-assets.s3.eu-central-1.amazonaws.com/company_logo_light.svg"
14
+ alt="TNG Logo"
15
+ width="400"
16
+ style="display: inline-block; vertical-align: middle;"/>
17
+ </div>
18
+ <br>
19
+ <div align="center">
20
+ <a href="https://huggingface.co/tngtech/DeepSeek-TNG-R1T2-Chimera/blob/main/LICENSE.DeepSeek" style="margin: 2px;">
21
+ <img alt="License" src="https://img.shields.io/badge/License-MIT-f5de53?&color=f5de53" style="display: inline-block; vertical-align: middle;"/>
22
+ </a>
23
+ </div>
24
+ <br>
25
+ <div align="center">
26
+ <img alt="Intelligence Score" src="intelligence_score_vs_output_tokens.png" style="display: inline-block; vertical-align: middle;" width="750"/>
27
+ <figcaption><a href="https://x.com/tngtech/status/1940531045432283412">Release Announcement on X</a></figcaption>
28
+ </div>
29
+
30
+
31
+ ## Assembly of Experts Chimera model constructed with the DeepSeek [R1-0528](https://huggingface.co/deepseek-ai/DeepSeek-R1-0528), [R1](https://huggingface.co/deepseek-ai/DeepSeek-R1) and [V3-0324](https://huggingface.co/deepseek-ai/DeepSeek-V3-0324) parent models
32
+
33
+ We present our new **DeepSeek-TNG R1T2 Chimera** 671B model, the first successor to our original [*DeepSeek R1T Chimera*](https://huggingface.co/tngtech/DeepSeek-R1T-Chimera) that was released on April 26th. Unlike the original Chimera, which was based on the *two parent models* V3-0324 and R1, the new Chimera is a **Tri-Mind** *with three parents*, namely additionally R1-0528. It is constructed using the Assembly of Experts-method with relatively fine-granular direct brain edits. This more refined assembly allowed, among other improvements, the fixing of the &lt;think&gt; token consistency issue, which was a weakness of R1T and is now solved for R1T2.
34
+
35
+ **Sweet spot**
36
+
37
+ R1T2 operates at a new sweet spot in intelligence vs. output token length. It appears to be...
38
+
39
+ - about **20% faster than** the regular **R1**, and more than **twice as fast as R1-0528**
40
+ - significantly **more intelligent than** the regular **R1** in benchmarks such as **GPQA**, **AIME-24** and **Aider Polyglot**
41
+ - much **more intelligent** and also **think-token consistent** compared to the first **R1T Chimera** 0426
42
+ - and generally well-behaved and a **nice persona** to talk to, even without any system prompt.
43
+
44
+ **Recommendations for your model decision**
45
+
46
+ *R1T2* compared...
47
+ - *vs R1:* We hope that R1T2 is a very desirable, almost universally **better drop-in replacement for R1**
48
+ - *vs R1-0528:* R1T2 is a much **cheaper alternative to the full R1-0528**, if the full 0528-level intelligence is not required
49
+ - *vs R1T:* R1T2 is usually **recommended over R1T**, unless the specific personality of R1T was optimal, the think-token issue not important, or R1T's higher speed crucial
50
+ - *vs V3-0324:* V3 is so much faster that if you can live with the **lower intelligence, take V3**, however, if you **need reasoning, R1T2** is the go-to model
51
+
52
+ **Limitations**
53
+
54
+ - **R1-0528** is thinking much longer, but also is achieving **better hard benchmark results** than R1T2
55
+ - As measured by SpeechMap.ai (courtesy of xlr8harder), **R1T2** is significantly **more reserved** than R1T, but not as much as R1-0528
56
+ - When switching from R1T to R1T2 development, we changed from AIME24 and MT-Bench to AIME24, AIME25 and GPQA-Diamond for the intelligence score. With the new benchmark set, there is a larger score difference between R1 and the original R1T Chimera than published earlier.
57
+ - Function calling is supported in general, but both vLLM and SGLang currently require some specific adaptions, see the section below.
58
+
59
+ **Evaluation results**
60
+
61
+ Evaluation was performed using the evalchemy framework (pass@1 averaged over 10/5 runs for AIME/GPQAD, at a temperature of 0.6).
62
+ We report measured benchmark results for our R1T2, R1T models and published benchmark results for V3-0324, R1, R1-0528.
63
+
64
+ | | R1T2 | R1T | V3-0324 | R1 | R1-0528 | Comment | Special source |
65
+ |:-----------------------------------|-----:|-----:|--------:|-----:|--------:|:--------|:--------|
66
+ | AIME-24 | 82.3 | 74.7 | 59.4 | 79.8 | 91.4 | | |
67
+ | AIME-25 | 70.0 | 58.3 | 49.6 | 70.0 | 87.5 | | V3-0324 AIME-25 measured by us |
68
+ | GPQA-Diamond | 77.9 | 72.0 | 68.4 | 71.5 | 81.0 | | |
69
+ | Aider Polyglot | 64.4 | 48.4 | 44.9 | 52.0 | 71.6 | R1T2 beats two of its parents, V3-0324 and R1, and was measured to be about 2.2 times more token efficient, i.e. faster, than its third parent, R1-0528 | R1T2 source: Aider discord, t=0.75 |
70
+ | MMLU-Pro Computer Science | 83.7-85.6 | 82.9-84.6 | 81.5-82.4 | 85.1-85.3 | 84.6-86.1 | | |
71
+ | EQ-Bench Longform Creative Writing | 76.4 | ./. | 78.1 | 74.6 | 78.9 | EQ Bench version before August 8th, 2025 | see [EQ Bench](https://eqbench.com/creative_writing_longform.html) |
72
+ | Vectara Hallucination Rate | 5.5 | ./. | 8.0 | 14.3 | 7.7 | lower hallucination rates are better, R1T2 is better than all its three parents | see [Hallucination Leaderboard](https://github.com/vectara/hallucination-leaderboard) |
73
+
74
+ ## Technological background
75
+
76
+ For details on the AoE construction process, you can read our [Paper on arXiV](https://arxiv.org/abs/2506.14794).
77
+
78
+ **Runtime parameter settings**
79
+
80
+ - Most of our evaluation was done with a maximum context size of 60,000 tokens.
81
+ With a context size of 130,000 tokens, the model proved very helpful in interpreting very long debug logs. Long-context testing was less extensive, though.
82
+ - We're running the model using vLLM on 8xH200 and MI325X nodes, additionally we've tested the model using SGLang, which is also used by [chutes.ai](https://chutes.ai/app/chute/4fa0c7f5-82f7-59d1-8996-661bb778893d).
83
+ - For SGLang, we recommend using versions >= v0.4.8 in combination with argument `--reasoning-parser qwen3` to properly handle rare cases when the model skips the `<think>` reasoning step.
84
+
85
+
86
+ ### Function calling
87
+
88
+ R1T2 does support function calling using an updated chat template (since 01 Aug 2025). However, neither vLLM nor SGLang provide an R1T2-compatible tool call parser natively but require some adaptions.
89
+
90
+ _vLLM:_
91
+
92
+ For function calling with vLLM, a new tool parser is required. While we opened [a PR to vLLM](https://github.com/vllm-project/vllm/pull/22074) to include an R1T2-compatible tool parser off-the-shelf, we also ship the tool parser file `tool_parser_vllm.py` within this repository.
93
+ With this file, tool calling can be enabled via
94
+ ```
95
+ --tool-parser-plugin <ABSOLUTE_MODEL_SNAPSHOT_PATH>/tool_parser_vllm.py \
96
+ --tool-call-parser tng_r1t2
97
+ ```
98
+
99
+ Here, put in the path to the snapshot folder such as `~/.cache/huggingface/hub/models--tngtech--DeepSeek-TNG-R1T2-Chimera/snapshots/SNAPSHOT/tool_parser_vllm.py`
100
+
101
+ _SGLang:_
102
+
103
+ Tool call support for R1T2 requires a recent SGLang version >= v0.4.10 (alternatively, you need to patch [this bugfix for the reasoning parser](https://github.com/sgl-project/sglang/pull/8606) for older versions of SGLang).
104
+
105
+ An R1T2-compatible tool call parser will be added with [this PR to SGLang](https://github.com/sgl-project/sglang/pull/8672).
106
+ Unfortunately, and unlike vLLM, there is no simple plugin system for tool call parsers in SGLang.
107
+ Until our PR is merged an relased with a new SGLang version, you can still install it manually by patching your SGLang source code as outlined in the PR:
108
+ The new tool call parser must be added and registered (so in total one file must be added, a second one edited, see [details here](https://github.com/sgl-project/sglang/pull/8672/files)).
109
+
110
+ Once the SGLang installation has been updated correctly, tool calling with R1T2 can be activated by starting SGLang with
111
+
112
+ ```
113
+ --tool-call-parser tng_r1t2
114
+ ```
115
+
116
+
117
+ ## Model Details
118
+
119
+ - **Architecture**: DeepSeek-MoE transformer-based language model
120
+ - **Combination Method**: Assembly of Experts from the three DeepSeek parent models R1-0528, R1 and V3-0324
121
+ - **Release Date**: 2025-07-02
122
+ - **Design Team**: Robert Dahlke, Henrik Klagges, Benjamin Merkel, Fabian Klemm and David Reiss, Munich, Germany
123
+ - **Extra Thanks**: Big thanks to DeepSeek for their great models and open-source generosity, and to the other researchers that have published on model merging methodologies.
124
+
125
+
126
+ ## Use, Out-of-scope Use, Other Limitations, Risks, Recommendations et al.
127
+ Regarding the R1T/R1T2-Chimeras, we ask you to follow the careful guidelines that Microsoft has created for their "MAI-DS-R1" DeepSeek-based model.
128
+ These professional guidelines are available [here on Hugging Face](https://huggingface.co/microsoft/MAI-DS-R1).
129
+
130
+ ## EU AI Act
131
+
132
+ Due to the strict new guidelines of the EU AI Act that take effect on August 2nd 2025, we recommend that each R1T/R1T2 user in the EU either familiarizes themselves with these requirements and assess their compliance, or ceases using the model in the EU after August 1st, 2025.
133
+
134
+ ## Contact, especially for your user feedback
135
+
136
+ Please give us your feedback, especially if you find deficiencies in the model:
137
+ - Email: [email protected]
138
+ - X.com: @tngtech
139
+
140
+ ## Citation
141
+
142
+ ```
143
+ @misc{tng_technology_consulting_gmbh_2025_07_02,
144
+ author = { TNG Technology Consulting GmbH },
145
+ title = { DeepSeek-TNG-R1T2-Chimera },
146
+ year = 2025,
147
+ month = { July },
148
+ url = { https://huggingface.co/tngtech/DeepSeek-TNG-R1T2-Chimera },
149
+ doi = { 10.57967/hf/5950 },
150
+ publisher = { Hugging Face }
151
+ }
152
+ ```
config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "/data/hf/hub/models--tngtech--DeepSeek-TNG-R1T2-Chimera/snapshots/c0f05b5836356f60dee8ac364394ceb28215e7ba",
3
+ "add_cross_attention": false,
4
+ "architectures": [
5
+ "DeepseekV3ForCausalLMNextN"
6
+ ],
7
+ "attention_bias": false,
8
+ "attention_dropout": 0.0,
9
+ "auto_map": {
10
+ "AutoConfig": "configuration_deepseek.DeepseekV3Config",
11
+ "AutoModel": "modeling_deepseek.DeepseekV3Model",
12
+ "AutoModelForCausalLM": "modeling_deepseek.DeepseekV3ForCausalLM"
13
+ },
14
+ "bad_words_ids": null,
15
+ "begin_suppress_tokens": null,
16
+ "bos_token_id": 0,
17
+ "chunk_size_feed_forward": 0,
18
+ "cross_attention_hidden_size": null,
19
+ "decoder_start_token_id": null,
20
+ "diversity_penalty": 0.0,
21
+ "do_sample": false,
22
+ "dtype": "bfloat16",
23
+ "early_stopping": false,
24
+ "encoder_no_repeat_ngram_size": 0,
25
+ "eos_token_id": 1,
26
+ "ep_size": 1,
27
+ "exponential_decay_length_penalty": null,
28
+ "finetuning_task": null,
29
+ "first_k_dense_replace": 3,
30
+ "forced_bos_token_id": null,
31
+ "forced_eos_token_id": null,
32
+ "hidden_act": "silu",
33
+ "hidden_size": 7168,
34
+ "id2label": {
35
+ "0": "LABEL_0",
36
+ "1": "LABEL_1"
37
+ },
38
+ "initializer_range": 0.02,
39
+ "intermediate_size": 18432,
40
+ "is_decoder": false,
41
+ "is_encoder_decoder": false,
42
+ "kv_lora_rank": 512,
43
+ "label2id": {
44
+ "LABEL_0": 0,
45
+ "LABEL_1": 1
46
+ },
47
+ "length_penalty": 1.0,
48
+ "max_length": 20,
49
+ "max_position_embeddings": 163840,
50
+ "min_length": 0,
51
+ "model_type": "deepseek_v3",
52
+ "moe_intermediate_size": 2048,
53
+ "moe_layer_freq": 1,
54
+ "n_group": 8,
55
+ "n_routed_experts": 256,
56
+ "n_shared_experts": 1,
57
+ "no_repeat_ngram_size": 0,
58
+ "norm_topk_prob": true,
59
+ "num_attention_heads": 128,
60
+ "num_beam_groups": 1,
61
+ "num_beams": 1,
62
+ "num_experts_per_tok": 8,
63
+ "num_hidden_layers": 1,
64
+ "num_key_value_heads": 128,
65
+ "num_nextn_predict_layers": 1,
66
+ "num_return_sequences": 1,
67
+ "output_attentions": false,
68
+ "output_hidden_states": false,
69
+ "output_scores": false,
70
+ "pad_token_id": null,
71
+ "prefix": null,
72
+ "problem_type": null,
73
+ "pruned_heads": {},
74
+ "q_lora_rank": 1536,
75
+ "qk_nope_head_dim": 128,
76
+ "qk_rope_head_dim": 64,
77
+ "quantization_config": {
78
+ "activation_scheme": "dynamic",
79
+ "fmt": "e4m3",
80
+ "quant_method": "fp8",
81
+ "weight_block_size": [
82
+ 128,
83
+ 128
84
+ ]
85
+ },
86
+ "remove_invalid_values": false,
87
+ "repetition_penalty": 1.0,
88
+ "return_dict": true,
89
+ "return_dict_in_generate": false,
90
+ "rms_norm_eps": 1e-06,
91
+ "rope_scaling": {
92
+ "beta_fast": 32,
93
+ "beta_slow": 1,
94
+ "factor": 40,
95
+ "mscale": 1.0,
96
+ "mscale_all_dim": 1.0,
97
+ "original_max_position_embeddings": 4096,
98
+ "type": "yarn"
99
+ },
100
+ "rope_theta": 10000,
101
+ "routed_scaling_factor": 2.5,
102
+ "scoring_func": "sigmoid",
103
+ "sep_token_id": null,
104
+ "suppress_tokens": null,
105
+ "task_specific_params": null,
106
+ "temperature": 1.0,
107
+ "tf_legacy_loss": false,
108
+ "tie_encoder_decoder": false,
109
+ "tie_word_embeddings": false,
110
+ "tokenizer_class": null,
111
+ "top_k": 50,
112
+ "top_p": 1.0,
113
+ "topk_group": 4,
114
+ "topk_method": "noaux_tc",
115
+ "torchscript": false,
116
+ "transformers_version": "4.56.1",
117
+ "typical_p": 1.0,
118
+ "use_bfloat16": false,
119
+ "use_cache": true,
120
+ "v_head_dim": 128,
121
+ "vocab_size": 129280
122
+ }
configuration_deepseek.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers.configuration_utils import PretrainedConfig
2
+ from transformers.utils import logging
3
+
4
+ logger = logging.get_logger(__name__)
5
+
6
+ DEEPSEEK_PRETRAINED_CONFIG_ARCHIVE_MAP = {}
7
+ class DeepseekV3Config(PretrainedConfig):
8
+ r"""
9
+ This is the configuration class to store the configuration of a [`DeepseekV3Model`]. It is used to instantiate an DeepSeek
10
+ model according to the specified arguments, defining the model architecture. Instantiating a configuration with the
11
+ defaults will yield a similar configuration to that of the DeepSeek-V3.
12
+
13
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
14
+ documentation from [`PretrainedConfig`] for more information.
15
+
16
+
17
+ Args:
18
+ vocab_size (`int`, *optional*, defaults to 129280):
19
+ Vocabulary size of the Deep model. Defines the number of different tokens that can be represented by the
20
+ `inputs_ids` passed when calling [`DeepseekV3Model`]
21
+ hidden_size (`int`, *optional*, defaults to 4096):
22
+ Dimension of the hidden representations.
23
+ intermediate_size (`int`, *optional*, defaults to 11008):
24
+ Dimension of the MLP representations.
25
+ moe_intermediate_size (`int`, *optional*, defaults to 1407):
26
+ Dimension of the MoE representations.
27
+ num_hidden_layers (`int`, *optional*, defaults to 32):
28
+ Number of hidden layers in the Transformer decoder.
29
+ num_nextn_predict_layers (`int`, *optional*, defaults to 1):
30
+ Number of nextn predict layers in the DeepSeekV3 Model.
31
+ num_attention_heads (`int`, *optional*, defaults to 32):
32
+ Number of attention heads for each attention layer in the Transformer decoder.
33
+ n_shared_experts (`int`, *optional*, defaults to None):
34
+ Number of shared experts, None means dense model.
35
+ n_routed_experts (`int`, *optional*, defaults to None):
36
+ Number of routed experts, None means dense model.
37
+ routed_scaling_factor (`float`, *optional*, defaults to 1.0):
38
+ Scaling factor or routed experts.
39
+ topk_method (`str`, *optional*, defaults to `gready`):
40
+ Topk method used in routed gate.
41
+ n_group (`int`, *optional*, defaults to None):
42
+ Number of groups for routed experts.
43
+ topk_group (`int`, *optional*, defaults to None):
44
+ Number of selected groups for each token(for each token, ensuring the selected experts is only within `topk_group` groups).
45
+ num_experts_per_tok (`int`, *optional*, defaults to None):
46
+ Number of selected experts, None means dense model.
47
+ moe_layer_freq (`int`, *optional*, defaults to 1):
48
+ The frequency of the MoE layer: one expert layer for every `moe_layer_freq - 1` dense layers.
49
+ first_k_dense_replace (`int`, *optional*, defaults to 0):
50
+ Number of dense layers in shallow layers(embed->dense->dense->...->dense->moe->moe...->lm_head).
51
+ \--k dense layers--/
52
+ norm_topk_prob (`bool`, *optional*, defaults to False):
53
+ Whether to normalize the weights of the routed experts.
54
+ scoring_func (`str`, *optional*, defaults to 'softmax'):
55
+ Method of computing expert weights.
56
+ aux_loss_alpha (`float`, *optional*, defaults to 0.001):
57
+ Auxiliary loss weight coefficient.
58
+ seq_aux = (`bool`, *optional*, defaults to True):
59
+ Whether to compute the auxiliary loss for each individual sample.
60
+ num_key_value_heads (`int`, *optional*):
61
+ This is the number of key_value heads that should be used to implement Grouped Query Attention. If
62
+ `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if
63
+ `num_key_value_heads=1 the model will use Multi Query Attention (MQA) otherwise GQA is used. When
64
+ converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
65
+ by meanpooling all the original heads within that group. For more details checkout [this
66
+ paper](https://arxiv.org/pdf/2305.13245.pdf). If it is not specified, will default to
67
+ `num_attention_heads`.
68
+ hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
69
+ The non-linear activation function (function or string) in the decoder.
70
+ max_position_embeddings (`int`, *optional*, defaults to 2048):
71
+ The maximum sequence length that this model might ever be used with.
72
+ initializer_range (`float`, *optional*, defaults to 0.02):
73
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
74
+ rms_norm_eps (`float`, *optional*, defaults to 1e-06):
75
+ The epsilon used by the rms normalization layers.
76
+ use_cache (`bool`, *optional*, defaults to `True`):
77
+ Whether or not the model should return the last key/values attentions (not used by all models). Only
78
+ relevant if `config.is_decoder=True`.
79
+ pad_token_id (`int`, *optional*):
80
+ Padding token id.
81
+ bos_token_id (`int`, *optional*, defaults to 1):
82
+ Beginning of stream token id.
83
+ eos_token_id (`int`, *optional*, defaults to 2):
84
+ End of stream token id.
85
+ tie_word_embeddings (`bool`, *optional*, defaults to `False`):
86
+ Whether to tie weight embeddings
87
+ rope_theta (`float`, *optional*, defaults to 10000.0):
88
+ The base period of the RoPE embeddings.
89
+ rope_scaling (`Dict`, *optional*):
90
+ Dictionary containing the scaling configuration for the RoPE embeddings. Currently supports two scaling
91
+ strategies: linear and dynamic. Their scaling factor must be a float greater than 1. The expected format is
92
+ `{"type": strategy name, "factor": scaling factor}`. When using this flag, don't update
93
+ `max_position_embeddings` to the expected new maximum.
94
+ attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
95
+ Whether to use a bias in the query, key, value and output projection layers during self-attention.
96
+ attention_dropout (`float`, *optional*, defaults to 0.0):
97
+ The dropout ratio for the attention probabilities.
98
+
99
+ ```python
100
+ >>> from transformers import DeepseekV3Model, DeepseekV3Config
101
+
102
+ >>> # Initializing a Deepseek-V3 style configuration
103
+ >>> configuration = DeepseekV3Config()
104
+
105
+ >>> # Accessing the model configuration
106
+ >>> configuration = model.config
107
+ ```"""
108
+
109
+ model_type = "deepseek_v3"
110
+ keys_to_ignore_at_inference = ["past_key_values"]
111
+
112
+ def __init__(
113
+ self,
114
+ vocab_size=129280,
115
+ hidden_size=7168,
116
+ intermediate_size=18432,
117
+ moe_intermediate_size = 2048,
118
+ num_hidden_layers=61,
119
+ num_nextn_predict_layers=1,
120
+ num_attention_heads=128,
121
+ num_key_value_heads=128,
122
+ n_shared_experts = 1,
123
+ n_routed_experts = 256,
124
+ ep_size = 1,
125
+ routed_scaling_factor = 2.5,
126
+ kv_lora_rank = 512,
127
+ q_lora_rank = 1536,
128
+ qk_rope_head_dim = 64,
129
+ v_head_dim = 128,
130
+ qk_nope_head_dim = 128,
131
+ topk_method = 'noaux_tc',
132
+ n_group = 8,
133
+ topk_group = 4,
134
+ num_experts_per_tok = 8,
135
+ moe_layer_freq = 1,
136
+ first_k_dense_replace = 3,
137
+ norm_topk_prob = True,
138
+ scoring_func = 'sigmoid',
139
+ hidden_act="silu",
140
+ max_position_embeddings=4096,
141
+ initializer_range=0.02,
142
+ rms_norm_eps=1e-6,
143
+ use_cache=True,
144
+ pad_token_id=None,
145
+ bos_token_id=0,
146
+ eos_token_id=1,
147
+ tie_word_embeddings=False,
148
+ rope_theta=10000.0,
149
+ rope_scaling=None,
150
+ attention_bias=False,
151
+ attention_dropout=0.0,
152
+ **kwargs,
153
+ ):
154
+ self.vocab_size = vocab_size
155
+ self.max_position_embeddings = max_position_embeddings
156
+ self.hidden_size = hidden_size
157
+ self.intermediate_size = intermediate_size
158
+ self.moe_intermediate_size = moe_intermediate_size
159
+ self.num_hidden_layers = num_hidden_layers
160
+ self.num_nextn_predict_layers = num_nextn_predict_layers
161
+ self.num_attention_heads = num_attention_heads
162
+ self.n_shared_experts = n_shared_experts
163
+ self.n_routed_experts = n_routed_experts
164
+ self.ep_size = ep_size
165
+ self.routed_scaling_factor = routed_scaling_factor
166
+ self.kv_lora_rank = kv_lora_rank
167
+ self.q_lora_rank = q_lora_rank
168
+ self.qk_rope_head_dim = qk_rope_head_dim
169
+ self.v_head_dim = v_head_dim
170
+ self.qk_nope_head_dim = qk_nope_head_dim
171
+ self.topk_method = topk_method
172
+ self.n_group = n_group
173
+ self.topk_group = topk_group
174
+ self.num_experts_per_tok = num_experts_per_tok
175
+ self.moe_layer_freq = moe_layer_freq
176
+ self.first_k_dense_replace = first_k_dense_replace
177
+ self.norm_topk_prob = norm_topk_prob
178
+ self.scoring_func = scoring_func
179
+ # for backward compatibility
180
+ if num_key_value_heads is None:
181
+ num_key_value_heads = num_attention_heads
182
+
183
+ self.num_key_value_heads = num_key_value_heads
184
+ self.hidden_act = hidden_act
185
+ self.initializer_range = initializer_range
186
+ self.rms_norm_eps = rms_norm_eps
187
+ self.use_cache = use_cache
188
+ self.rope_theta = rope_theta
189
+ self.rope_scaling = rope_scaling
190
+ self.attention_bias = attention_bias
191
+ self.attention_dropout = attention_dropout
192
+
193
+ super().__init__(
194
+ pad_token_id=pad_token_id,
195
+ bos_token_id=bos_token_id,
196
+ eos_token_id=eos_token_id,
197
+ tie_word_embeddings=tie_word_embeddings,
198
+ **kwargs,
199
+ )
intelligence_score_vs_output_tokens.png ADDED

Git LFS Details

  • SHA256: ace1e8df27abccaf153f01b719117cbc024839c02cab6e2a300aa401ba196af7
  • Pointer size: 131 Bytes
  • Size of remote file: 197 kB
model.safetensors.index.json ADDED
The diff for this file is too large to render. See raw diff
 
nextn_layer_parameters.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22b0f714c7fd5bfc0b7c10761c73f3dff0e2d7c6a383b784a7a9cad4c51e2012
3
+ size 11717707368
tensor_types.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": true,
3
+ "add_eos_token": false,
4
+ "bos_token": {
5
+ "__type": "AddedToken",
6
+ "content": "<|begin▁of▁sentence|>",
7
+ "lstrip": false,
8
+ "normalized": true,
9
+ "rstrip": false,
10
+ "single_word": false
11
+ },
12
+ "clean_up_tokenization_spaces": false,
13
+ "eos_token": {
14
+ "__type": "AddedToken",
15
+ "content": "<|end▁of▁sentence|>",
16
+ "lstrip": false,
17
+ "normalized": true,
18
+ "rstrip": false,
19
+ "single_word": false
20
+ },
21
+ "legacy": true,
22
+ "model_max_length": 131072,
23
+ "pad_token": {
24
+ "__type": "AddedToken",
25
+ "content": "<|end▁of▁sentence|>",
26
+ "lstrip": false,
27
+ "normalized": true,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ },
31
+ "sp_model_kwargs": {},
32
+ "unk_token": null,
33
+ "tokenizer_class": "LlamaTokenizerFast",
34
+ "chat_template": "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true, is_last_user=false) %}{%- for message in messages %}{%- if message['role'] == 'system' %}{%- if ns.is_first_sp %}{% set ns.system_prompt = ns.system_prompt + message['content'] %}{% set ns.is_first_sp = false %}{%- else %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + message['content'] %}{%- endif %}{%- endif %}{%- endfor -%}{#- Adapted from https://github.com/sgl-project/sglang/blob/main/examples/chat_template/tool_chat_template_deepseekr1.jinja #}{% if tools is defined and tools is not none %}{% set tool_ns = namespace(text='You are a helpful assistant with tool calling capabilities. When a tool call is needed, you MUST use the following format to issue the call:\\n<tool_call>\\n{\"name\": FUNCTION_NAME, \"arguments\": {\"param1\": \"value1\", \"param2\": \"value2\"}}\\n</tool_call>\\n\\nMake sure the JSON is valid.\\n\\n## Tools\\n\\n### Function\\n\\nYou have the following functions available:\\n\\n') %}{% for tool in tools %}{% set tool_ns.text = tool_ns.text + '\\n- ' + tool['function']['name'] + '\\n```json\\n' + (tool['function'] | tojson) + '\\n```\\n' %}{% endfor %}{% set ns.system_prompt = ns.system_prompt + '\\n\\n' + tool_ns.text %}{% endif %}{{- bos_token }}{{- ns.system_prompt }}{%- for message in messages %}{% set content = message['content'] %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{%- set ns.is_first = false -%}{%- set ns.is_last_user = true -%}{{'<|User|>' + content + '<|Assistant|>'}}{%- endif %}{%- if message['role'] == 'assistant' %}{% if '</think>' in content %}{% set content = content.split('</think>')[-1] %}{% endif %}{% endif %}{%- if message['role'] == 'assistant' and message['tool_calls'] is defined and message['tool_calls'] is not none %}{%- set ns.is_last_user = false -%}{%- if ns.is_tool %}{{- ''}}{%- endif %}{%- set ns.is_first = false %}{%- set ns.is_tool = false -%}{%- set ns.is_output_first = true %}{%- for tool in message['tool_calls'] %}{%- if tool['function']['arguments'] is string %}{%- set arguments = tool['function']['arguments'] %}{%- else %}{%- set arguments = tool['function']['arguments'] | tojson %}{%- endif %}{%- if not ns.is_first %}{%- if content is none %}{{- '<tool_call>\\n{\"tool_call_id\": ' + tool['id']|tojson + ', \"name\": ' + tool['function']['name']|tojson + ', \"arguments\": ' + arguments + '}\\n</tool_call>'}}{%- else %}{{- content + '\\n\\n<tool_call>\\n{\"tool_call_id\": ' + tool['id']|tojson + ', \"name\": ' + tool['function']['name']|tojson + ', \"arguments\": ' + arguments + '}\\n</tool_call>'}}{%- endif %}{%- set ns.is_first = true -%}{%- else %}{{- '\\n\\n<tool_call>\\n{\"tool_call_id\": ' + tool['id']|tojson + ', \"name\": ' + tool['function']['name']|tojson + ', \"arguments\": ' + arguments + '}\\n</tool_call>'}}{%- endif %}{%- endfor %}{{- '<|end▁of▁sentence|>'}}{%- endif %}{%- if message['role'] == 'assistant' and (message['tool_calls'] is not defined or message['tool_calls'] is none)%}{%- set ns.is_last_user = false -%}{%- if ns.is_tool %}{{- '\\n' + content + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{{- content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_last_user = false -%}{%- set ns.is_tool = true -%}{%- set tool_call_id_param = '' %}{%- if message['tool_call_id'] %}{%- set tool_call_id_param = '\"tool_call_id\": ' + message['tool_call_id']|tojson + ', ' %}{%- endif %}{%- if ns.is_output_first %}{{- '\\n\\n{' + tool_call_id_param + '\"content\": ' + content|tojson + '}'}}{%- set ns.is_output_first = false %}{%- else %}{{- '\\n{' + tool_call_id_param + '\"content\": ' + content|tojson + '}'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{- '\\n\\n'}}{%- endif %}{% if add_generation_prompt and not ns.is_last_user %}{{- '<|Assistant|>'}}{%- endif %}"
35
+ }
tool_chat_template.jinja ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if not add_generation_prompt is defined %}
2
+ {% set add_generation_prompt = false %}
3
+ {% endif %}
4
+ {% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='', is_first_sp=true, is_last_user=false) %}
5
+ {%- for message in messages %}
6
+ {%- if message['role'] == 'system' %}
7
+ {%- if ns.is_first_sp %}
8
+ {% set ns.system_prompt = ns.system_prompt + message['content'] %}
9
+ {% set ns.is_first_sp = false %}
10
+ {%- else %}
11
+ {% set ns.system_prompt = ns.system_prompt + '\n\n' + message['content'] %}
12
+ {%- endif %}
13
+ {%- endif %}
14
+ {%- endfor -%}
15
+
16
+ {#- Adapted from https://github.com/sgl-project/sglang/blob/main/examples/chat_template/tool_chat_template_deepseekr1.jinja #}
17
+ {% if tools is defined and tools is not none %}
18
+ {% set tool_ns = namespace(text='You are a helpful assistant with tool calling capabilities. '
19
+ 'When a tool call is needed, you MUST use the following format to issue the call:\n'
20
+ '<tool_call>\n{"name": FUNCTION_NAME, "arguments": {"param1": "value1", "param2": "value2"}}\n</tool_call>\n\n'
21
+ 'Make sure the JSON is valid.\n\n'
22
+ '## Tools\n\n### Function\n\nYou have the following functions available:\n\n') %}
23
+ {% for tool in tools %}
24
+ {% set tool_ns.text = tool_ns.text + '\n- ' + tool['function']['name'] + '\n```json\n' + (tool['function'] | tojson) + '\n```\n' %}
25
+ {% endfor %}
26
+ {% set ns.system_prompt = ns.system_prompt + '\n\n' + tool_ns.text %}
27
+ {% endif %}
28
+
29
+ {{- bos_token }}
30
+ {{- ns.system_prompt }}
31
+ {%- for message in messages %}
32
+ {% set content = message['content'] %}
33
+ {%- if message['role'] == 'user' %}
34
+ {%- set ns.is_tool = false -%}
35
+ {%- set ns.is_first = false -%}
36
+ {%- set ns.is_last_user = true -%}
37
+ {{'<|User|>' + content + '<|Assistant|>'}}
38
+ {%- endif %}
39
+ {%- if message['role'] == 'assistant' %}
40
+ {% if '</think>' in content %}
41
+ {% set content = content.split('</think>')[-1] %}
42
+ {% endif %}
43
+ {% endif %}
44
+ {%- if message['role'] == 'assistant' and message['tool_calls'] is defined and message['tool_calls'] is not none %}
45
+ {%- set ns.is_last_user = false -%}
46
+ {%- if ns.is_tool %}
47
+ {{- ''}}
48
+ {%- endif %}
49
+ {%- set ns.is_first = false %}
50
+ {%- set ns.is_tool = false -%}
51
+ {%- set ns.is_output_first = true %}
52
+ {%- for tool in message['tool_calls'] %}
53
+ {%- if tool['function']['arguments'] is string %}
54
+ {%- set arguments = tool['function']['arguments'] %}
55
+ {%- else %}
56
+ {%- set arguments = tool['function']['arguments'] | tojson %}
57
+ {%- endif %}
58
+ {%- if not ns.is_first %}
59
+ {%- if content is none %}
60
+ {{- '<tool_call>\n{"tool_call_id": ' + tool['id']|tojson + ', "name": ' + tool['function']['name']|tojson + ', "arguments": ' + arguments + '}\n</tool_call>'}}
61
+ {%- else %}
62
+ {{- content + '\n\n<tool_call>\n{"tool_call_id": ' + tool['id']|tojson + ', "name": ' + tool['function']['name']|tojson + ', "arguments": ' + arguments + '}\n</tool_call>'}}
63
+ {%- endif %}
64
+ {%- set ns.is_first = true -%}
65
+ {%- else %}
66
+ {{- '\n\n<tool_call>\n{"tool_call_id": ' + tool['id']|tojson + ', "name": ' + tool['function']['name']|tojson + ', "arguments": ' + arguments + '}\n</tool_call>'}}
67
+ {%- endif %}
68
+ {%- endfor %}
69
+ {{- '<|end▁of▁sentence|>'}}
70
+ {%- endif %}
71
+ {%- if message['role'] == 'assistant' and (message['tool_calls'] is not defined or message['tool_calls'] is none)%}
72
+ {%- set ns.is_last_user = false -%}
73
+ {%- if ns.is_tool %}
74
+ {{- '\n' + content + '<|end▁of▁sentence|>'}}
75
+ {%- set ns.is_tool = false -%}
76
+ {%- else %}
77
+ {{- content + '<|end▁of▁sentence|>'}}
78
+ {%- endif %}
79
+ {%- endif %}
80
+ {%- if message['role'] == 'tool' %}
81
+ {%- set ns.is_last_user = false -%}
82
+ {%- set ns.is_tool = true -%}
83
+ {%- set tool_call_id_param = '' %}
84
+ {%- if message['tool_call_id'] %}
85
+ {%- set tool_call_id_param = '"tool_call_id": ' + message['tool_call_id']|tojson + ', ' %}
86
+ {%- endif %}
87
+ {%- if ns.is_output_first %}
88
+ {{- '\n\n{' + tool_call_id_param + '"content": ' + content|tojson + '}'}}
89
+ {%- set ns.is_output_first = false %}
90
+ {%- else %}
91
+ {{- '\n{' + tool_call_id_param + '"content": ' + content|tojson + '}'}}
92
+ {%- endif %}
93
+ {%- endif %}
94
+ {%- endfor -%}
95
+ {% if ns.is_tool %}
96
+ {{- '\n\n'}}
97
+ {%- endif %}
98
+ {% if add_generation_prompt and not ns.is_last_user %}
99
+ {{- '<|Assistant|>'}}
100
+ {%- endif %}
tool_parser_vllm.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ # ruff: noqa
4
+ import json
5
+ from collections.abc import Sequence
6
+ from enum import Enum
7
+ from typing import Any, Union
8
+
9
+ import partial_json_parser
10
+ import regex as re
11
+ from partial_json_parser.core.options import Allow
12
+
13
+ from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
14
+ DeltaFunctionCall, DeltaMessage,
15
+ DeltaToolCall,
16
+ ExtractedToolCallInformation,
17
+ FunctionCall, ToolCall)
18
+ from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import (
19
+ ToolParser, ToolParserManager)
20
+ from vllm.logger import init_logger
21
+ from vllm.transformers_utils.tokenizer import AnyTokenizer
22
+ from vllm.utils import random_uuid
23
+
24
+ logger = init_logger(__name__)
25
+
26
+
27
+ class ParsedStructure(Enum):
28
+ CONTENT = 1
29
+ REASONING_CONTENT = 2
30
+ TOOL_CALL = 3
31
+ TOOL_CALL_DELIMITER = 4
32
+ TOOL_CALL_START_TAG = 5
33
+ TOOL_CALL_END_TAG = 6
34
+
35
+
36
+ @ToolParserManager.register_module("tng_r1t2")
37
+ class TngR1T2ToolParser(ToolParser):
38
+ """Tool Parser for models like tngtech/DeepSeek-TNG-R1T2-Chimera,
39
+ It is compatible with hermes tool call templates
40
+ but does not require <tool_call> and </tool_call>
41
+ to be single tokens in the vocabulary;
42
+ instead only the string representation of the model output
43
+ is parsed, making this tool call parser robust and versatile."""
44
+
45
+ def __init__(self, tokenizer: AnyTokenizer):
46
+ super().__init__(tokenizer)
47
+
48
+ # For backward compatibility with serving code
49
+ self.prev_tool_call_arr: list[dict] = []
50
+ self.streamed_args_for_tool: list[str] = []
51
+
52
+ self.think_tag_pattern = r"(<think>[\s\S]*?</think>)"
53
+ self.think_start_tag = "<think>"
54
+ self.think_end_tag = "</think>"
55
+
56
+ self.tool_call_tag_pattern = r"<tool_call>([\s\S]*?)</tool_call>"
57
+ self.tool_call_start_tag = "<tool_call>"
58
+ self.tool_call_end_tag = "</tool_call>"
59
+
60
+ # Define streaming state type to be initialized later
61
+ self.streaming_state: dict[str, Any] = {
62
+ "streamed_tool_calls": [],
63
+ "buffer": "",
64
+ "parsed_structure": ParsedStructure.CONTENT,
65
+ }
66
+
67
+ def extract_tool_call_from_nonthink_output(
68
+ self, raw_text: str) -> tuple[str, list[dict]]:
69
+ parts = re.split(self.tool_call_tag_pattern, raw_text)
70
+ content = ""
71
+ tool_calls: list[dict] = []
72
+ for i, part in enumerate(parts):
73
+ is_potential_tool_call = i % 2 == 1
74
+ if is_potential_tool_call:
75
+ try:
76
+ more_tool_calls = json.loads(part)
77
+ if isinstance(more_tool_calls, list):
78
+ tool_calls.extend(more_tool_calls)
79
+ else:
80
+ tool_calls.extend([more_tool_calls])
81
+ except json.JSONDecodeError:
82
+ logger.warning("Invalid tool call json "
83
+ "-> parse as text content")
84
+ content += part
85
+ continue
86
+ else:
87
+ content += part
88
+ return content, tool_calls
89
+
90
+ def extract_tool_calls(
91
+ self,
92
+ model_output: str,
93
+ request: ChatCompletionRequest,
94
+ ) -> ExtractedToolCallInformation:
95
+ """
96
+ Extract tool calls from a complete model output.
97
+ """
98
+ # split at think traces -> those will not be parsed for tool calls
99
+ think_parts = re.split(self.think_tag_pattern, model_output)
100
+ content = ""
101
+ tool_calls = []
102
+ for i, part in enumerate(think_parts):
103
+ parse_output = i % 2 == 0
104
+ if parse_output:
105
+ more_content, more_tool_calls = (
106
+ self.extract_tool_call_from_nonthink_output(part))
107
+ content += more_content
108
+ tool_calls += more_tool_calls
109
+ else:
110
+ content += part
111
+
112
+ if not tool_calls:
113
+ return ExtractedToolCallInformation(
114
+ tools_called=False,
115
+ tool_calls=[],
116
+ content=content,
117
+ )
118
+
119
+ tool_call_objs: list[ToolCall] = []
120
+
121
+ for idx, call in enumerate(tool_calls):
122
+ if (not isinstance(call, dict) or "name" not in call
123
+ or "arguments" not in call):
124
+ logger.warning("Invalid tool call format, ignore.")
125
+ continue
126
+
127
+ tool_call = ToolCall(
128
+ id=f"call_{idx}_{random_uuid()}",
129
+ type="function",
130
+ function=FunctionCall(
131
+ name=call["name"],
132
+ arguments=(json.dumps(call["arguments"]) if isinstance(
133
+ call["arguments"], dict) else call["arguments"]),
134
+ ),
135
+ )
136
+ tool_call_objs.append(tool_call)
137
+
138
+ return ExtractedToolCallInformation(
139
+ tools_called=len(tool_call_objs) > 0,
140
+ tool_calls=tool_call_objs,
141
+ content=content,
142
+ )
143
+
144
+ def _parse_think_trace(self, raw_text: str) -> tuple[str, bool, str]:
145
+ """
146
+ Returns: (unambiguous_text_content, found_think_end, rest_string)
147
+ """
148
+ # Either a complete think_end_tag can be somewhere in raw_text
149
+ think_end_pos = raw_text.find(self.think_end_tag)
150
+ if think_end_pos >= 0:
151
+ # in contrast to tool_call_start_tags, </think> remains part of content
152
+ think_end_tag_end_pos = think_end_pos + len(self.think_end_tag)
153
+ return (raw_text[:think_end_tag_end_pos], True,
154
+ raw_text[think_end_tag_end_pos:])
155
+
156
+ # or the end of raw_text can be continued to a complete think_end_tag
157
+ think_end_pos = (
158
+ len(raw_text) -
159
+ self._ends_with_partial_token(raw_text, self.think_end_tag))
160
+ return raw_text[:think_end_pos], False, raw_text[think_end_pos:]
161
+
162
+ def _parse_unambiguous_text_content(
163
+ self, raw_text: str) -> tuple[str, Union[str, None], str]:
164
+ """
165
+ Returns: (unambiguous_text_content, interrupting_tag, rest_string)
166
+ """
167
+ # Either a complete tool_call_start_tag or think_start can be somewhere in raw_text
168
+ search_tags = [self.think_start_tag, self.tool_call_start_tag]
169
+ tag_positions = [(tag, pos) for tag in search_tags
170
+ if (pos := raw_text.find(tag)) >= 0]
171
+ tag_positions.sort(key=lambda tag_and_pos: tag_and_pos[1])
172
+ if len(tag_positions) > 0:
173
+ first_tag, tag_pos = tag_positions[0]
174
+ return raw_text[:tag_pos], first_tag, raw_text[tag_pos:]
175
+
176
+ # or the end of raw_text can be continued to a complete tag
177
+ tag_positions = [
178
+ (tag, len(raw_text) - self._ends_with_partial_token(raw_text, tag))
179
+ for tag in search_tags
180
+ ]
181
+ tag_positions.sort(key=lambda tag_and_pos: tag_and_pos[1])
182
+ first_tag, tag_pos = tag_positions[0]
183
+ if tag_pos < len(raw_text):
184
+ return raw_text[:tag_pos], None, raw_text[tag_pos:]
185
+ return raw_text, None, ""
186
+
187
+ def _parse_tool_call_start_tag(self, raw_text: str) -> tuple[bool, str]:
188
+ """
189
+ Removes tool_call_start_tag from the beginning of raw_text,
190
+ and an optional "[", and leading whitespace.
191
+ Returns: (found_complete_tool_call_start_tag, rest_string)
192
+ """
193
+ if not raw_text.startswith(self.tool_call_start_tag):
194
+ return False, raw_text
195
+ rest = raw_text[len(self.tool_call_start_tag):].lstrip()
196
+ if rest.startswith("["):
197
+ rest = rest[1:].lstrip()
198
+ return True, rest
199
+
200
+ def _parse_tool_call_end_tag(
201
+ self, raw_text: str) -> tuple[Union[bool, None], str]:
202
+ """
203
+ Removes tool_call_end_tag from the beginning of raw_text,
204
+ and an optional "]" before it, and leading whitespace.
205
+ Returns: tuple
206
+ found_complete_tool_call_end_tag (or None if not decidable yet)
207
+ rest_string
208
+ """
209
+ # remove optional whitespace and closing ] bracket from json list notation
210
+ rest = raw_text.lstrip()
211
+ if rest.startswith("]"):
212
+ rest = rest[1:].lstrip()
213
+
214
+ if rest.startswith(self.tool_call_end_tag):
215
+ # found a complete tool call end tag
216
+ return True, rest[len(self.tool_call_end_tag):]
217
+ if (len(rest) >= len(self.tool_call_end_tag)
218
+ or rest != self.tool_call_end_tag[:len(rest)]):
219
+ # evidence that rest_string does not start with a tool call end tag
220
+ return False, raw_text
221
+ # incomplete tool call end tag, can not be decided yet
222
+ return None, raw_text
223
+
224
+ def _extract_arguments_from_partial_tool_call(
225
+ self, raw_text: str) -> Union[str, None]:
226
+ """
227
+ Extracts the raw text of the "arguments" field of a complete
228
+ or partial tool call.
229
+ Args:
230
+ raw_text: tool call raw text,
231
+ e.g `{"name": "my_tool", "arguments": {"firstarg": "some`
232
+
233
+ Returns:
234
+ raw text of the "arguments" field, which is not valid JSON
235
+ unless the tool call is complete,
236
+ e.g. `{"firstarg": "some` for the example raw_text above
237
+ """
238
+ # assumptions:
239
+ # - "arguments" is always an object
240
+ # - there is no other field of type object in the function call
241
+ # - `raw_text` contains first "name", then "arguments" (otherwise,
242
+ # we'd have to find the end of "arguments" before returning its
243
+ # raw text value)
244
+
245
+ # typically, at position 0, but there might be leading whitespace
246
+ tool_call_start_pos = raw_text.find("{")
247
+ assert raw_text[:tool_call_start_pos].strip() == ""
248
+ arguments_start_pos = raw_text.find("{", tool_call_start_pos + 1)
249
+ if arguments_start_pos < 0:
250
+ return None
251
+ arguments_raw_text = raw_text[arguments_start_pos:]
252
+ return arguments_raw_text
253
+
254
+ def _parse_complete_tool_call(
255
+ self, raw_text: str) -> tuple[Union[dict, None], str]:
256
+ """
257
+ Returns: tuple
258
+ parsed tool call if complete, None otherwise
259
+ rest_string that needs to be parsed again or may contain
260
+ a partial tool call
261
+ """
262
+ # raw_text must start without whitespace for correct parsing
263
+ obj, end_pos = self.extract_complete_json_dict(raw_text)
264
+ if obj is None:
265
+ return None, raw_text
266
+ tool_call_raw_text = raw_text[:end_pos]
267
+ # `tool_call_raw_text` is something like:
268
+ # '{"name": "tool-name", "arguments": {...xyz...} }'
269
+ # we want to extract `{...xyz...}`,
270
+ # but `extract_arguments_from_partial_tool_call` would return
271
+ # everything after the second '{', i.e. `{...xyz...} }`
272
+ arguments_raw_text = self._extract_arguments_from_partial_tool_call(
273
+ tool_call_raw_text.removesuffix("}").rstrip())
274
+ tool_call = {
275
+ "name": obj.get("name"),
276
+ "arguments": obj.get("arguments"),
277
+ "arguments_raw_text": arguments_raw_text,
278
+ "is_complete": True,
279
+ }
280
+ return tool_call, raw_text[end_pos:]
281
+
282
+ def _parse_partial_tool_call(self, raw_text: str) -> Union[dict, None]:
283
+ # raw_text must start without whitespace for correct parsing
284
+ obj = partial_json_parser.loads(raw_text, Allow.ALL)
285
+ arguments_raw_text = (
286
+ self._extract_arguments_from_partial_tool_call(raw_text))
287
+ tool_call = {
288
+ "name": obj.get("name"),
289
+ "arguments": obj.get("arguments"),
290
+ "arguments_raw_text": arguments_raw_text,
291
+ "is_complete": False,
292
+ }
293
+ return tool_call
294
+
295
+ def _parse_tool_call(
296
+ self,
297
+ raw_text: str) -> tuple[Union[bool, None], Union[dict, None], str]:
298
+ # remove optional whitespace and closing ] bracket
299
+ # from json list notation
300
+ rest = raw_text.lstrip()
301
+ if rest == "":
302
+ # no json has been received yet
303
+ # -> can't tell if this will be a valid tool call
304
+ return None, None, raw_text
305
+ if not rest.startswith("{"):
306
+ # can't be a tool call json
307
+ return False, None, raw_text
308
+
309
+ tool_call, rest = self._parse_complete_tool_call(rest)
310
+ if tool_call:
311
+ return True, tool_call, rest
312
+
313
+ try:
314
+ tool_call = self._parse_partial_tool_call(rest)
315
+ # need to re-parse partial tool call later again
316
+ # -> return None, not True
317
+ return None, tool_call, rest
318
+ except json.JSONDecodeError:
319
+ # invalid json -> neither complete nor partial tool call
320
+ return False, None, rest
321
+
322
+ def _parse_tool_call_delimiter(
323
+ self, raw_text: str) -> tuple[Union[bool, None], str]:
324
+ """
325
+ Returns: tuple
326
+ does raw_text start with tool call delimiter?
327
+ (None if undecidable/incomplete)
328
+ rest_string
329
+ """
330
+ rest = raw_text.lstrip()
331
+ if rest == "":
332
+ return None, raw_text
333
+ has_next_tool_call = rest.startswith(",")
334
+ if not has_next_tool_call:
335
+ return False, raw_text
336
+
337
+ rest = rest[1:].lstrip()
338
+ if rest == "":
339
+ return None, raw_text
340
+ has_next_tool_call = rest.startswith("{")
341
+ if not has_next_tool_call:
342
+ return False, raw_text
343
+ return True, rest
344
+
345
+ def _parse_all(
346
+ self, raw_text: str, start_mode: ParsedStructure
347
+ ) -> tuple[str, list[dict], str, ParsedStructure]:
348
+ if start_mode == ParsedStructure.REASONING_CONTENT:
349
+ content, found_closing_think, rest = self._parse_think_trace(
350
+ raw_text)
351
+ if found_closing_think:
352
+ more_content, tool_calls, rest, structure = self._parse_all(
353
+ rest, start_mode=ParsedStructure.CONTENT)
354
+ return content + more_content, tool_calls, rest, structure
355
+ return content, [], rest, ParsedStructure.REASONING_CONTENT
356
+
357
+ elif start_mode == ParsedStructure.CONTENT:
358
+ content, interrupting_tag, rest = self._parse_unambiguous_text_content(
359
+ raw_text)
360
+
361
+ # rest might contain a tool call start tag or a think start tag
362
+ if interrupting_tag == self.tool_call_start_tag:
363
+ more_content, tool_calls, rest, structure = self._parse_all(
364
+ rest, start_mode=ParsedStructure.TOOL_CALL_START_TAG)
365
+ return content + more_content, tool_calls, rest, structure
366
+ elif interrupting_tag == self.think_start_tag:
367
+ more_content, tool_calls, rest, structure = self._parse_all(
368
+ rest, start_mode=ParsedStructure.REASONING_CONTENT)
369
+ return content + more_content, tool_calls, rest, structure
370
+ else:
371
+ return content, [], rest, ParsedStructure.CONTENT
372
+
373
+ elif start_mode == ParsedStructure.TOOL_CALL_START_TAG:
374
+ found_tool_call_start_tag, rest = self._parse_tool_call_start_tag(
375
+ raw_text)
376
+ if not found_tool_call_start_tag:
377
+ return "", [], raw_text, ParsedStructure.CONTENT
378
+ # we found a complete start tag, but we haven't seen the begin of a tool call json yet
379
+ content, tool_calls, rest, structure = self._parse_all(
380
+ rest, start_mode=ParsedStructure.TOOL_CALL)
381
+ if not content and not tool_calls:
382
+ # We haven't reached the opening "{" of the tool call yet.
383
+ # We might see a "[" before the "{", so let's process the start tag again next chunk.
384
+ return content, [], raw_text, ParsedStructure.CONTENT
385
+ return content, tool_calls, rest, structure
386
+
387
+ elif start_mode == ParsedStructure.TOOL_CALL:
388
+ found_tool_call, tool_call, rest = self._parse_tool_call(raw_text)
389
+ if found_tool_call is True:
390
+ tool_calls = [tool_call] if tool_call else []
391
+ content, more_tool_calls, rest, structure = self._parse_all(
392
+ rest, start_mode=ParsedStructure.TOOL_CALL_DELIMITER)
393
+ return (content, tool_calls + more_tool_calls, rest, structure)
394
+ elif found_tool_call is None:
395
+ # partial tool call -> need to parse again with next chunk
396
+ tool_calls = ([tool_call] if tool_call is not None else [])
397
+ return "", tool_calls, rest, ParsedStructure.TOOL_CALL
398
+ else:
399
+ logger.warning(
400
+ "Invalid tool call -> continue with parsing model output as text content"
401
+ )
402
+ return self._parse_all(raw_text,
403
+ start_mode=ParsedStructure.CONTENT)
404
+
405
+ elif start_mode == ParsedStructure.TOOL_CALL_DELIMITER:
406
+ found_tool_call_delimiter, rest = self._parse_tool_call_delimiter(
407
+ raw_text)
408
+ if found_tool_call_delimiter is True:
409
+ return self._parse_all(rest,
410
+ start_mode=ParsedStructure.TOOL_CALL)
411
+ elif found_tool_call_delimiter is None:
412
+ # could neither confirm nor deny that raw_text starts with a tool call delimiter
413
+ return "", [], rest, ParsedStructure.TOOL_CALL_DELIMITER
414
+ else:
415
+ return self._parse_all(
416
+ raw_text, start_mode=ParsedStructure.TOOL_CALL_END_TAG)
417
+
418
+ elif start_mode == ParsedStructure.TOOL_CALL_END_TAG:
419
+ found_tool_call_end_tag, rest = self._parse_tool_call_end_tag(
420
+ raw_text)
421
+ if found_tool_call_end_tag is True:
422
+ return self._parse_all(rest,
423
+ start_mode=ParsedStructure.CONTENT)
424
+ elif found_tool_call_end_tag is None:
425
+ return "", [], rest, ParsedStructure.TOOL_CALL_END_TAG
426
+ else:
427
+ return self._parse_all(raw_text,
428
+ start_mode=ParsedStructure.CONTENT)
429
+
430
+ logger.warning(
431
+ f"Unknown tool call parser start_mode '{start_mode}'. Falling back to text content."
432
+ )
433
+ return self._parse_all(raw_text, start_mode=ParsedStructure.CONTENT)
434
+
435
+ def extract_tool_calls_streaming(
436
+ self,
437
+ previous_text: str,
438
+ current_text: str,
439
+ delta_text: str,
440
+ previous_token_ids: Sequence[int],
441
+ current_token_ids: Sequence[int],
442
+ delta_token_ids: Sequence[int],
443
+ request: ChatCompletionRequest,
444
+ ) -> Union[DeltaMessage, None]:
445
+ """
446
+ Extract tool calls for streaming mode.
447
+ """
448
+ raw_text = self.streaming_state["buffer"] + delta_text
449
+ structure = self.streaming_state["parsed_structure"]
450
+
451
+ content, tool_calls, rest, new_structure = self._parse_all(
452
+ raw_text, start_mode=structure)
453
+ self.streaming_state["buffer"] = rest
454
+ self.streaming_state["parsed_structure"] = new_structure
455
+
456
+ already_streamed_tool_calls = (
457
+ self.streaming_state["streamed_tool_calls"])
458
+ already_streamed_complete_tool_calls = [
459
+ tool_call for tool_call in already_streamed_tool_calls
460
+ if tool_call["is_complete"]
461
+ ]
462
+ all_tool_calls = (already_streamed_complete_tool_calls +
463
+ (tool_calls or []))
464
+ to_be_streamed_tool_calls = self._calculate_delta_tool_calls(
465
+ all_tool_calls, already_streamed_tool_calls)
466
+
467
+ if not content and not to_be_streamed_tool_calls:
468
+ return None
469
+ self.update_state_vars(all_tool_calls)
470
+ return DeltaMessage(content=content if content else None,
471
+ tool_calls=to_be_streamed_tool_calls)
472
+
473
+ def _calculate_delta_tool_calls(
474
+ self, current_tool_calls: Union[list[dict], None],
475
+ already_streamed_tool_calls: list[dict]) -> list[DeltaToolCall]:
476
+ if not current_tool_calls:
477
+ return []
478
+
479
+ new_deltas = []
480
+ for tool_call_idx, partial_tool_call in enumerate(current_tool_calls):
481
+ if (partial_tool_call.get("name") is None
482
+ or partial_tool_call.get("arguments") is None):
483
+ # do not stream arguments for an unknown tool name;
484
+ # and unless arguments appear in the partial json,
485
+ # it might be that "name" has not been received completely
486
+ # (assuming a template like `{"name": "mytool", "arguments": ...}`)
487
+ continue
488
+ partial_tool_call["tool_call_idx"] = tool_call_idx
489
+ partial_tool_call["arguments_raw_text"] = (
490
+ partial_tool_call.get('arguments_raw_text') or "")
491
+
492
+ if len(already_streamed_tool_calls) > tool_call_idx:
493
+ # parts of this tool_call_idx have already been streamed
494
+ already_streamed_tool_call = already_streamed_tool_calls[
495
+ tool_call_idx]
496
+ delta_tool_call = self._delta_for_partial_tool_call(
497
+ partial_tool_call, already_streamed_tool_call)
498
+ if delta_tool_call is not None:
499
+ new_deltas.append(delta_tool_call)
500
+ already_streamed_tool_calls[tool_call_idx] = (
501
+ already_streamed_tool_call | partial_tool_call)
502
+ else:
503
+ # no parts of this tool_call_idx have been streamed yet
504
+ tool_call = self._delta_for_new_tool_call(partial_tool_call)
505
+ new_deltas.append(tool_call)
506
+ already_streamed_tool_calls.append(partial_tool_call)
507
+
508
+ return new_deltas
509
+
510
+ def _delta_for_new_tool_call(self, tool_call_dict: dict) -> DeltaToolCall:
511
+ """constructs DeltaToolCall for new tool call,
512
+ with tool_call_id, name, and all arguments seen so far.
513
+ Updates tool_call dictionary with tool_call_id.
514
+ """
515
+ tool_call_idx = tool_call_dict["tool_call_idx"]
516
+ tool_call_dict[
517
+ "tool_call_id"] = f"call_{tool_call_idx}_{random_uuid()}"
518
+ tool_call_dict["arguments_raw_text"] = tool_call_dict.get(
519
+ 'arguments_raw_text') or ""
520
+ delta_tool_call = DeltaToolCall(
521
+ index=tool_call_idx,
522
+ type="function",
523
+ id=tool_call_dict["tool_call_id"],
524
+ function=DeltaFunctionCall(
525
+ name=tool_call_dict.get("name"),
526
+ arguments=tool_call_dict["arguments_raw_text"]))
527
+ return delta_tool_call
528
+
529
+ def _delta_for_partial_tool_call(
530
+ self, new_tool_call: dict,
531
+ already_streamed_tool_call: dict) -> Union[DeltaToolCall, None]:
532
+ """Calculate delta for a tool call of which some parts have already been streamed."""
533
+ assert new_tool_call["name"] == already_streamed_tool_call["name"]
534
+ assert already_streamed_tool_call.get("tool_call_id")
535
+ if already_streamed_tool_call.get("is_complete"):
536
+ return None
537
+
538
+ to_be_streamed_arguments = (
539
+ new_tool_call["arguments_raw_text"].removeprefix(
540
+ already_streamed_tool_call["arguments_raw_text"]))
541
+ if not to_be_streamed_arguments:
542
+ return None
543
+
544
+ delta_tool_call = DeltaToolCall(
545
+ index=new_tool_call["tool_call_idx"],
546
+ type="function",
547
+ function=DeltaFunctionCall(arguments=to_be_streamed_arguments))
548
+ return delta_tool_call
549
+
550
+ def update_state_vars(self, all_tools: list[dict]) -> None:
551
+ # `tool_parser.streamed_args_for_tool` and
552
+ # `tool_parser.prev_tool_call_arr` are checked in serving_chat.py
553
+
554
+ # relevant is {"arguments": {...}}
555
+ self.prev_tool_call_arr = all_tools
556
+
557
+ # json-serialized argument
558
+ self.streamed_args_for_tool = [
559
+ tool_call.get("arguments_raw_text", "") for tool_call in all_tools
560
+ ]
561
+
562
+ @classmethod
563
+ def _ends_with_partial_token(cls, buffer: str, tag: str) -> int:
564
+ """
565
+ Check if buffer ends with a partial tag.
566
+ Return the length of the partial tag.
567
+ """
568
+ for i in range(1, min(len(buffer) + 1, len(tag))):
569
+ if tag.startswith(buffer[-i:]):
570
+ return i
571
+ return 0
572
+
573
+ @classmethod
574
+ def extract_complete_json_dict(cls, json_str: str):
575
+ try:
576
+ decoder = json.JSONDecoder()
577
+ obj, end_pos = decoder.raw_decode(
578
+ json_str) # ignore any text after the end of the json object
579
+ if isinstance(obj, dict):
580
+ return obj, end_pos
581
+ return None, 0
582
+ except json.JSONDecodeError:
583
+ return None, 0