Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- LICENSE.DeepSeek +21 -0
- README.md +152 -0
- config.json +122 -0
- configuration_deepseek.py +199 -0
- intelligence_score_vs_output_tokens.png +3 -0
- model.safetensors.index.json +0 -0
- nextn_layer_parameters.safetensors +3 -0
- tensor_types.json +1 -0
- tokenizer.json +0 -0
- tokenizer_config.json +35 -0
- tool_chat_template.jinja +100 -0
- tool_parser_vllm.py +583 -0
.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 <think> 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
|
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
|