Add files using upload-large-folder tool
Browse files- README.md +348 -0
- llama_nemotron_toolcall_parser_no_streaming.py +470 -0
README.md
ADDED
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
library_name: transformers
|
3 |
+
license: other
|
4 |
+
license_name: nvidia-open-model-license
|
5 |
+
license_link: >-
|
6 |
+
https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/
|
7 |
+
|
8 |
+
pipeline_tag: text-generation
|
9 |
+
language:
|
10 |
+
- en
|
11 |
+
tags:
|
12 |
+
- nvidia
|
13 |
+
- llama-3
|
14 |
+
- pytorch
|
15 |
+
---
|
16 |
+
|
17 |
+
# Llama-3.3-Nemotron-Super-49B-v1.5-FP8
|
18 |
+
|
19 |
+

|
20 |
+
|
21 |
+
## Model Overview
|
22 |
+
|
23 |
+
Llama-3.3-Nemotron-Super-49B-v1.5-FP8 is a significantly upgraded version of Llama-3.3-Nemotron-Super-49B-v1 and is a large language model (LLM) which is a derivative of Meta Llama-3.3-70B-Instruct (AKA the reference model). It is a reasoning model that is post trained for reasoning, human chat preferences, and agentic tasks, such as RAG and tool calling. The model supports a context length of 128K tokens.
|
24 |
+
|
25 |
+
Llama-3.3-Nemotron-Super-49B-v1.5-FP8 is a model which offers a great tradeoff between model accuracy and efficiency. Efficiency (throughput) directly translates to savings. Using a novel Neural Architecture Search (NAS) approach, we greatly reduce the model’s memory footprint, enabling larger workloads, as well as fitting the model on a single GPU at high workloads (H200). This NAS approach enables the selection of a desired point in the accuracy-efficiency tradeoff. For more information on the NAS approach, please refer to [this paper](https://arxiv.org/abs/2411.19146)
|
26 |
+
|
27 |
+
The model underwent a multi-phase post-training process to enhance both its reasoning and non-reasoning capabilities. This includes a supervised fine-tuning stage for Math, Code, Science, and Tool Calling. Additionally, the model went through multiple stages of Reinforcement Learning (RL) including Reward-aware Preference Optimization (RPO) for chat, Reinforcement Learning with Verifiable Rewards (RLVR) for reasoning, and iterative Direct Preference Optimization (DPO) for Tool Calling capability enhancements. The final checkpoint was achieved after merging several RL and DPO checkpoints.
|
28 |
+
|
29 |
+
This model is part of the Llama Nemotron Collection. You can find the other model(s) in this family here:
|
30 |
+
- [Llama-3.1-Nemotron-Nano-4B-v1.1](https://huggingface.co/nvidia/Llama-3.1-Nemotron-Nano-4B-v1.1)
|
31 |
+
- [Llama-3.1-Nemotron-Ultra-253B-v1](https://huggingface.co/nvidia/Llama-3_1-Nemotron-Ultra-253B-v1)
|
32 |
+
|
33 |
+
This model is ready for commercial use.
|
34 |
+
|
35 |
+
## License/Terms of Use
|
36 |
+
|
37 |
+
GOVERNING TERMS: Your use of this model is governed by the [NVIDIA Open Model License.](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/) Additional Information: [Llama 3.3 Community License Agreement](https://www.llama.com/llama3_3/license/). Built with Llama.
|
38 |
+
|
39 |
+
**Model Developer:** NVIDIA
|
40 |
+
|
41 |
+
**Model Dates:** Trained between November 2024 and July 2025
|
42 |
+
|
43 |
+
**Data Freshness:** The pretraining data has a cutoff of 2023 per Meta Llama 3.3 70B
|
44 |
+
|
45 |
+
## Deployment Geography
|
46 |
+
Global
|
47 |
+
|
48 |
+
### Use Case: <br>
|
49 |
+
Developers designing AI Agent systems, chatbots, RAG systems, and other AI-powered applications. Also suitable for typical instruction-following tasks. <br>
|
50 |
+
|
51 |
+
### Release Date: <br>
|
52 |
+
- Hugging Face 7/25/2025 via [Llama-3_3-Nemotron-Super-49B-v1_5](https://huggingface.co/nvidia/Llama-3_3-Nemotron-Super-49B-v1_5)
|
53 |
+
- build.nvidia.com 7/25/2025 [Llama-3_3-Nemotron-Super-49B-v1_5](https://build.nvidia.com/nvidia/llama-3_3-nemotron-super-49b-v1_5)
|
54 |
+
|
55 |
+
## References
|
56 |
+
|
57 |
+
* [\[2505.00949\] Llama-Nemotron: Efficient Reasoning Models](https://arxiv.org/abs/2505.00949)
|
58 |
+
* [\[2502.00203\] Reward-aware Preference Optimization: A Unified Mathematical Framework for Model Alignment](https://arxiv.org/abs/2502.00203)
|
59 |
+
* [\[2411.19146\]Puzzle: Distillation-Based NAS for Inference-Optimized LLMs](https://arxiv.org/abs/2411.19146)
|
60 |
+
|
61 |
+
## Model Architecture
|
62 |
+
|
63 |
+
**Architecture Type:** Dense decoder-only Transformer model
|
64 |
+
|
65 |
+
**Network Architecture:** Llama 3.3 70B Instruct, customized through Neural Architecture Search (NAS)
|
66 |
+
|
67 |
+
The model is a derivative of Meta’s Llama-3.3-70B-Instruct, using Neural Architecture Search (NAS). The NAS algorithm results in non-standard and non-repetitive blocks. This includes the following:
|
68 |
+
|
69 |
+
Skip attention: In some blocks, the attention is skipped entirely, or replaced with a single linear layer.
|
70 |
+
Variable FFN: The expansion/compression ratio in the FFN layer is different between blocks.
|
71 |
+
|
72 |
+
We utilize a block-wise distillation of the reference model, where for each block we create multiple variants providing different tradeoffs of quality vs. computational complexity, discussed in more depth below. We then search over the blocks to create a model which meets the required throughput and memory (optimized for a single H100-80GB GPU) while minimizing the quality degradation. The model then undergoes knowledge distillation (KD), with a focus on English single and multi-turn chat use-cases. The KD step included 40 billion tokens consisting of a mixture of 3 datasets - FineWeb, Buzz-V1.2 and Dolma.
|
73 |
+
|
74 |
+
## Intended use
|
75 |
+
|
76 |
+
Llama-3.3-Nemotron-Super-49B-v1.5-FP8 is a general purpose reasoning and chat model intended to be used in English and coding languages. Other non-English languages (German, French, Italian, Portuguese, Hindi, Spanish, and Thai) are also supported.
|
77 |
+
|
78 |
+
## Input
|
79 |
+
- **Input Type:** Text
|
80 |
+
- **Input Format:** String
|
81 |
+
- **Input Parameters:** One-Dimensional (1D)
|
82 |
+
- **Other Properties Related to Input:** Context length up to 131,072 tokens
|
83 |
+
|
84 |
+
## Output
|
85 |
+
- **Output Type:** Text
|
86 |
+
- **Output Format:** String
|
87 |
+
- **Output Parameters:** One-Dimensional (1D)
|
88 |
+
- **Other Properties Related to Output:** Context length up to 131,072 tokens
|
89 |
+
|
90 |
+
Our AI models are designed and/or optimized to run on NVIDIA GPU-accelerated systems. By leveraging NVIDIA’s hardware (e.g. GPU cores) and software frameworks (e.g., CUDA libraries), the model achieves faster training and inference times compared to CPU-only solutions.
|
91 |
+
|
92 |
+
## Model Version
|
93 |
+
1.5 (07/25/2025)
|
94 |
+
|
95 |
+
## Software Integration
|
96 |
+
- **Runtime Engine:** Transformers
|
97 |
+
|
98 |
+
- **Recommended Hardware Microarchitecture Compatibility:**
|
99 |
+
- NVIDIA Ampere
|
100 |
+
- NVIDIA Hopper
|
101 |
+
- **Preferred Operating System(s):** Linux
|
102 |
+
|
103 |
+
## Quick Start and Usage Recommendations:
|
104 |
+
|
105 |
+
1. By default (empty system prompt) the model will respond in reasoning ON mode. Setting `/no_think` in the system prompt will enable reasoning OFF mode.
|
106 |
+
2. We recommend setting temperature to `0.6`, and Top P to `0.95` for Reasoning ON mode
|
107 |
+
3. We recommend using greedy decoding for Reasoning OFF mode
|
108 |
+
|
109 |
+
You can try this model out through the preview API, using this link: [Llama-3_3-Nemotron-Super-49B-v1_5](https://build.nvidia.com/nvidia/llama-3_3-nemotron-super-49b-v1_5).
|
110 |
+
|
111 |
+
## Use It with vLLM
|
112 |
+
|
113 |
+
```pip install vllm==0.9.2```
|
114 |
+
|
115 |
+
An example on how to serve with vLLM:
|
116 |
+
|
117 |
+
```console
|
118 |
+
$ python3 -m vllm.entrypoints.openai.api_server \
|
119 |
+
--model "nvidia/Llama-3_3-Nemotron-Super-49B-v1_5-FP8" \
|
120 |
+
--trust-remote-code \
|
121 |
+
--seed=1 \
|
122 |
+
--host="0.0.0.0" \
|
123 |
+
--port=5000 \
|
124 |
+
--served-model-name "Llama-3_3-Nemotron-Super-49B-v1_5-FP8" \
|
125 |
+
--tensor-parallel-size=8 \
|
126 |
+
--max-model-len=65536 \
|
127 |
+
--gpu-memory-utilization 0.95 \
|
128 |
+
--enforce-eager \
|
129 |
+
--quantization=modelopt
|
130 |
+
```
|
131 |
+
|
132 |
+
### Running a vLLM Server with Tool-call Support
|
133 |
+
To enable tool calling usage with this model, we provide a tool parser in the repository. Here is an example on how to use it:
|
134 |
+
|
135 |
+
|
136 |
+
```console
|
137 |
+
$ git clone https://huggingface.co/nvidia/Llama-3_3-Nemotron-Super-49B-v1_5-FP8
|
138 |
+
|
139 |
+
$ conda create -n vllm python=3.12 -y
|
140 |
+
$ conda activate vllm
|
141 |
+
$ pip install vllm==0.9.2
|
142 |
+
|
143 |
+
$ python3 -m vllm.entrypoints.openai.api_server \
|
144 |
+
--model Llama-3_3-Nemotron-Super-49B-v1_5-FP8 \
|
145 |
+
--trust-remote-code \
|
146 |
+
--seed=1 \
|
147 |
+
--host="0.0.0.0" \
|
148 |
+
--port=5000 \
|
149 |
+
--served-model-name "Llama-3_3-Nemotron-Super-49B-v1_5-FP8" \
|
150 |
+
--tensor-parallel-size=8 \
|
151 |
+
--max-model-len=65536 \
|
152 |
+
--gpu-memory-utilization 0.95 \
|
153 |
+
--enforce-eager \
|
154 |
+
--quantization=modelopt \
|
155 |
+
--enable-auto-tool-choice \
|
156 |
+
--tool-parser-plugin "Llama-3_3-Nemotron-Super-49B-v1_5-FP8/llama_nemotron_toolcall_parser_no_streaming.py" \
|
157 |
+
--tool-call-parser "llama_nemotron_json"
|
158 |
+
```
|
159 |
+
|
160 |
+
After launching a vLLM server, you can call the server with tool-call support using a Python script like below.
|
161 |
+
|
162 |
+
```python
|
163 |
+
from openai import OpenAI
|
164 |
+
client = OpenAI(
|
165 |
+
base_url="http://0.0.0.0:5000/v1",
|
166 |
+
api_key="dummy",
|
167 |
+
)
|
168 |
+
completion = client.chat.completions.create(
|
169 |
+
model="Llama-3_3-Nemotron-Super-49B-v1_5-FP8",
|
170 |
+
messages=[
|
171 |
+
{"role": "system", "content": ""},
|
172 |
+
{"role": "user", "content": "My bill is $100. What will be the amount for 18% tip?"}
|
173 |
+
],
|
174 |
+
tools=[
|
175 |
+
{
|
176 |
+
"type": "function",
|
177 |
+
"function": {
|
178 |
+
"name": "calculate_tip",
|
179 |
+
"parameters": {
|
180 |
+
"type": "object",
|
181 |
+
"properties": {
|
182 |
+
"bill_total": {
|
183 |
+
"type": "integer",
|
184 |
+
"description": "The total amount of the bill"
|
185 |
+
},
|
186 |
+
"tip_percentage": {
|
187 |
+
"type": "integer",
|
188 |
+
"description": "The percentage of tip to be applied"
|
189 |
+
}
|
190 |
+
},
|
191 |
+
"required": ["bill_total", "tip_percentage"]
|
192 |
+
}
|
193 |
+
}
|
194 |
+
},
|
195 |
+
{
|
196 |
+
"type": "function",
|
197 |
+
"function": {
|
198 |
+
"name": "convert_currency",
|
199 |
+
"parameters": {
|
200 |
+
"type": "object",
|
201 |
+
"properties": {
|
202 |
+
"amount": {
|
203 |
+
"type": "integer",
|
204 |
+
"description": "The amount to be converted"
|
205 |
+
},
|
206 |
+
"from_currency": {
|
207 |
+
"type": "string",
|
208 |
+
"description": "The currency code to convert from"
|
209 |
+
},
|
210 |
+
"to_currency": {
|
211 |
+
"type": "string",
|
212 |
+
"description": "The currency code to convert to"
|
213 |
+
}
|
214 |
+
},
|
215 |
+
"required": ["from_currency", "amount", "to_currency"]
|
216 |
+
}
|
217 |
+
}
|
218 |
+
}
|
219 |
+
],
|
220 |
+
temperature=0.6,
|
221 |
+
top_p=0.95,
|
222 |
+
max_tokens=32768,
|
223 |
+
stream=False
|
224 |
+
)
|
225 |
+
print(completion.choices[0].message.content)
|
226 |
+
'''
|
227 |
+
<think>
|
228 |
+
Okay, let's see. The user has a bill of $100 and wants to know the amount for an 18% tip. Hmm, I need to calculate the tip based on the bill total and the percentage. The tools provided include calculate_tip, which takes bill_total and tip_percentage as parameters. So the bill_total here is 100, and the tip_percentage is 18. I should call the calculate_tip function with these values. Wait, do I need to check if the parameters are integers? The bill is $100, which is an integer, and 18% is also an integer. So that fits the function's requirements. I don't need to convert any currency here because the user is asking about a tip in the same currency. So the correct tool to use is calculate_tip with those parameters.
|
229 |
+
</think>
|
230 |
+
'''
|
231 |
+
print(completion.choices[0].message.tool_calls)
|
232 |
+
'''
|
233 |
+
[ChatCompletionMessageToolCall(id='chatcmpl-tool-e341c6954d2c48c2a0e9071c7bdefd8b', function=Function(arguments='{"bill_total": 100, "tip_percentage": 18}', name='calculate_tip'), type='function')]
|
234 |
+
'''
|
235 |
+
```
|
236 |
+
|
237 |
+
## Training and Evaluation Datasets
|
238 |
+
|
239 |
+
## Training Datasets
|
240 |
+
|
241 |
+
A large variety of training data was used for the knowledge distillation phase before post-training pipeline, 3 of which included: FineWeb, Buzz-V1.2, and Dolma.
|
242 |
+
|
243 |
+
The data for the multi-stage post-training phases for improvements in Code, Math, and Reasoning is a compilation of SFT and RL data that supports improvements of math, code, general reasoning, and instruction following capabilities of the original Llama instruct model.
|
244 |
+
|
245 |
+
Prompts have been sourced from either public and open corpus or synthetically generated. Responses were synthetically generated by a variety of models, with some prompts containing responses for both reasoning on and off modes, to train the model to distinguish between two modes.
|
246 |
+
|
247 |
+
NVIDIA will be releasing the post-training dataset in the coming weeks.
|
248 |
+
|
249 |
+
**Data Collection for Training Datasets:**
|
250 |
+
Hybrid: Automated, Human, Synthetic
|
251 |
+
|
252 |
+
**Data Labeling for Training Datasets:**
|
253 |
+
Hybrid: Automated, Human, Synthetic
|
254 |
+
|
255 |
+
## Evaluation Datasets
|
256 |
+
|
257 |
+
We used the datasets listed below to evaluate Llama-3.3-Nemotron-Super-49B-v1.5-FP8.
|
258 |
+
|
259 |
+
Data Collection for Evaluation Datasets:
|
260 |
+
- Hybrid: Human. Synthetic
|
261 |
+
|
262 |
+
Data Labeling for Evaluation Datasets:
|
263 |
+
- Hybrid: Human, Synthetic, Automatic
|
264 |
+
|
265 |
+
## Evaluation Results
|
266 |
+
We evaluate the model using temperature=`0.6`, top_p=`0.95`, and 64k sequence length. We run the benchmarks up to 16 times and average the scores to be more accurate.
|
267 |
+
|
268 |
+
### MATH500
|
269 |
+
|
270 |
+
| Reasoning Mode | pass@1 (avg. over 4 runs) |
|
271 |
+
|--------------|------------|
|
272 |
+
| Reasoning On | 97.8 |
|
273 |
+
|
274 |
+
### AIME 2024
|
275 |
+
|
276 |
+
| Reasoning Mode | pass@1 (avg. over 16 runs) |
|
277 |
+
|--------------|------------|
|
278 |
+
| Reasoning On | 88.54 |
|
279 |
+
|
280 |
+
### AIME 2025
|
281 |
+
|
282 |
+
| Reasoning Mode | pass@1 (avg. over 16 runs) |
|
283 |
+
|--------------|------------|
|
284 |
+
| Reasoning On | 83.75 |
|
285 |
+
|
286 |
+
### GPQA
|
287 |
+
|
288 |
+
| Reasoning Mode | pass@1 (avg. over 4 runs) |
|
289 |
+
|--------------|------------|
|
290 |
+
| Reasoning On | 69.57 |
|
291 |
+
|
292 |
+
### LiveCodeBench 24.10-25.02
|
293 |
+
|
294 |
+
| Reasoning Mode | pass@1 (avg. over 4 runs) |
|
295 |
+
|--------------|------------|
|
296 |
+
| Reasoning On | 69.52 |
|
297 |
+
|
298 |
+
### BFCL v3
|
299 |
+
|
300 |
+
| Reasoning Mode | pass@1 (avg. over 2 runs) |
|
301 |
+
|--------------|------------|
|
302 |
+
| Reasoning On | 71.11 |
|
303 |
+
|
304 |
+
### IFEval
|
305 |
+
|
306 |
+
| Reasoning Mode | Strict:Instruction |
|
307 |
+
|--------------|------------|
|
308 |
+
| Reasoning On | 87.05 |
|
309 |
+
|
310 |
+
### ArenaHard
|
311 |
+
|
312 |
+
| Reasoning Mode | pass@1 (avg. over 1 runs) |
|
313 |
+
|--------------|------------|
|
314 |
+
| Reasoning On | 88.7 |
|
315 |
+
|
316 |
+
|
317 |
+
All evaluations were done using the [NeMo-Skills](https://github.com/NVIDIA/NeMo-Skills) repository.
|
318 |
+
|
319 |
+
## Inference:
|
320 |
+
|
321 |
+
**Engine:**
|
322 |
+
- Transformers
|
323 |
+
|
324 |
+
**Test Hardware:**
|
325 |
+
- 1x NVIDIA H100-80GB GPU
|
326 |
+
|
327 |
+
## Ethical Considerations:
|
328 |
+
|
329 |
+
NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal model team to ensure this model meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
|
330 |
+
|
331 |
+
For more detailed information on ethical considerations for this model, please see the Model Card++ [Explainability](./EXPLAINABILITY.md), [Bias](./BIAS.md), [Safety & Security](./SAFETY&SECURITY.md), and [Privacy](./PRIVACY.md) Subcards.
|
332 |
+
|
333 |
+
Please report security vulnerabilities or NVIDIA AI Concerns [here](https://www.nvidia.com/en-us/support/submit-security-vulnerability/).
|
334 |
+
|
335 |
+
## Citation
|
336 |
+
|
337 |
+
```
|
338 |
+
@misc{bercovich2025llamanemotronefficientreasoningmodels,
|
339 |
+
title={Llama-Nemotron: Efficient Reasoning Models},
|
340 |
+
author={Akhiad Bercovich and Itay Levy and Izik Golan and Mohammad Dabbah and Ran El-Yaniv and Omri Puny and Ido Galil and Zach Moshe and Tomer Ronen and Najeeb Nabwani and Ido Shahaf and Oren Tropp and Ehud Karpas and Ran Zilberstein and Jiaqi Zeng and Soumye Singhal and Alexander Bukharin and Yian Zhang and Tugrul Konuk and Gerald Shen and Ameya Sunil Mahabaleshwarkar and Bilal Kartal and Yoshi Suhara and Olivier Delalleau and Zijia Chen and Zhilin Wang and David Mosallanezhad and Adi Renduchintala and Haifeng Qian and Dima Rekesh and Fei Jia and Somshubra Majumdar and Vahid Noroozi and Wasi Uddin Ahmad and Sean Narenthiran and Aleksander Ficek and Mehrzad Samadi and Jocelyn Huang and Siddhartha Jain and Igor Gitman and Ivan Moshkov and Wei Du and Shubham Toshniwal and George Armstrong and Branislav Kisacanin and Matvei Novikov and Daria Gitman and Evelina Bakhturina and Jane Polak Scowcroft and John Kamalu and Dan Su and Kezhi Kong and Markus Kliegl and Rabeeh Karimi and Ying Lin and Sanjeev Satheesh and Jupinder Parmar and Pritam Gundecha and Brandon Norick and Joseph Jennings and Shrimai Prabhumoye and Syeda Nahida Akter and Mostofa Patwary and Abhinav Khattar and Deepak Narayanan and Roger Waleffe and Jimmy Zhang and Bor-Yiing Su and Guyue Huang and Terry Kong and Parth Chadha and Sahil Jain and Christine Harvey and Elad Segal and Jining Huang and Sergey Kashirsky and Robert McQueen and Izzy Putterman and George Lam and Arun Venkatesan and Sherry Wu and Vinh Nguyen and Manoj Kilaru and Andrew Wang and Anna Warno and Abhilash Somasamudramath and Sandip Bhaskar and Maka Dong and Nave Assaf and Shahar Mor and Omer Ullman Argov and Scot Junkin and Oleksandr Romanenko and Pedro Larroy and Monika Katariya and Marco Rovinelli and Viji Balas and Nicholas Edelman and Anahita Bhiwandiwalla and Muthu Subramaniam and Smita Ithape and Karthik Ramamoorthy and Yuting Wu and Suguna Varshini Velury and Omri Almog and Joyjit Daw and Denys Fridman and Erick Galinkin and Michael Evans and Katherine Luna and Leon Derczynski and Nikki Pope and Eileen Long and Seth Schneider and Guillermo Siman and Tomasz Grzegorzek and Pablo Ribalta and Monika Katariya and Joey Conway and Trisha Saar and Ann Guan and Krzysztof Pawelec and Shyamala Prayaga and Oleksii Kuchaiev and Boris Ginsburg and Oluwatobi Olabiyi and Kari Briski and Jonathan Cohen and Bryan Catanzaro and Jonah Alben and Yonatan Geifman and Eric Chung and Chris Alexiuk},
|
341 |
+
year={2025},
|
342 |
+
eprint={2505.00949},
|
343 |
+
archivePrefix={arXiv},
|
344 |
+
primaryClass={cs.CL},
|
345 |
+
url={https://arxiv.org/abs/2505.00949},
|
346 |
+
}
|
347 |
+
```
|
348 |
+
|
llama_nemotron_toolcall_parser_no_streaming.py
ADDED
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# SPDX-License-Identifier: Apache-2.0
|
2 |
+
|
3 |
+
import ast
|
4 |
+
import json
|
5 |
+
import re
|
6 |
+
from collections.abc import Sequence
|
7 |
+
from typing import Union
|
8 |
+
|
9 |
+
import partial_json_parser
|
10 |
+
from partial_json_parser.core.options import Allow
|
11 |
+
|
12 |
+
from vllm.entrypoints.openai.protocol import (
|
13 |
+
ChatCompletionRequest,
|
14 |
+
DeltaFunctionCall, DeltaMessage,
|
15 |
+
DeltaToolCall,
|
16 |
+
ExtractedToolCallInformation,
|
17 |
+
FunctionCall,
|
18 |
+
ToolCall,
|
19 |
+
)
|
20 |
+
from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import (
|
21 |
+
ToolParser,
|
22 |
+
ToolParserManager,
|
23 |
+
)
|
24 |
+
from vllm.logger import init_logger
|
25 |
+
from vllm.transformers_utils.tokenizer import AnyTokenizer
|
26 |
+
from vllm.utils import random_uuid
|
27 |
+
|
28 |
+
logger = init_logger(__name__)
|
29 |
+
|
30 |
+
|
31 |
+
@ToolParserManager.register_module("llama_nemotron_xml")
|
32 |
+
class LlamaNemotronXMLToolParser(ToolParser):
|
33 |
+
|
34 |
+
def __init__(self, tokenizer: AnyTokenizer):
|
35 |
+
super().__init__(tokenizer)
|
36 |
+
|
37 |
+
self.current_tool_name_sent: bool = False
|
38 |
+
self.prev_tool_call_arr: list[dict] = []
|
39 |
+
self.current_tool_id: int = -1 # Potentially for streaming
|
40 |
+
self.streamed_args_for_tool: list[str] = [] # Potentially for streaming
|
41 |
+
|
42 |
+
self.tool_call_start_token: str = "<tool_call>"
|
43 |
+
self.tool_call_end_token: str = "</tool_call>"
|
44 |
+
|
45 |
+
# Regex to find full <tool_call>...</tool_call> blocks and capture their content
|
46 |
+
self.tool_call_block_regex = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
|
47 |
+
# Regex to find <tool>...</tool> within a tool_call block content
|
48 |
+
self.name_regex = re.compile(r"<tool>(.*?)</tool>", re.DOTALL)
|
49 |
+
# Regex to find <key>value</key> pairs within the tool_call block content (excluding <tool> tags)
|
50 |
+
self.param_regex = re.compile(r"<([^/>\s]+)>(.*?)</\1>", re.DOTALL)
|
51 |
+
|
52 |
+
def extract_tool_calls(
|
53 |
+
self,
|
54 |
+
model_output: str,
|
55 |
+
request: ChatCompletionRequest,
|
56 |
+
) -> ExtractedToolCallInformation:
|
57 |
+
|
58 |
+
tool_call_start_index = model_output.find(self.tool_call_start_token)
|
59 |
+
|
60 |
+
if tool_call_start_index == -1:
|
61 |
+
return ExtractedToolCallInformation(
|
62 |
+
tools_called=False,
|
63 |
+
tool_calls=[],
|
64 |
+
content=model_output,
|
65 |
+
)
|
66 |
+
|
67 |
+
content = model_output[:tool_call_start_index].strip()
|
68 |
+
tool_calls_str_content = model_output[tool_call_start_index:]
|
69 |
+
|
70 |
+
parsed_tool_calls = []
|
71 |
+
|
72 |
+
try:
|
73 |
+
# Find all occurrences of <tool_call>...</tool_call>
|
74 |
+
xml_tool_call_contents = self.tool_call_block_regex.findall(tool_calls_str_content)
|
75 |
+
|
76 |
+
for tool_content_str in xml_tool_call_contents:
|
77 |
+
name_match = self.name_regex.search(tool_content_str)
|
78 |
+
if not name_match:
|
79 |
+
logger.warning(f"Could not find tool name in XML block: {tool_content_str}")
|
80 |
+
continue
|
81 |
+
tool_name = name_match.group(1).strip()
|
82 |
+
|
83 |
+
parsed_arguments = {}
|
84 |
+
|
85 |
+
# Find all parameter tags in the tool_call content, excluding the <tool> tag
|
86 |
+
param_matches = self.param_regex.finditer(tool_content_str)
|
87 |
+
|
88 |
+
for match in param_matches:
|
89 |
+
param_name = match.group(1).strip()
|
90 |
+
param_value_str = match.group(2).strip()
|
91 |
+
|
92 |
+
# Skip the <tool> tag since it's not a parameter
|
93 |
+
if param_name == "tool":
|
94 |
+
continue
|
95 |
+
|
96 |
+
target_type = None
|
97 |
+
# Try to get type from request.tools schema
|
98 |
+
if request.tools:
|
99 |
+
for tool_def in request.tools:
|
100 |
+
if tool_def.function.name == tool_name:
|
101 |
+
if tool_def.function.parameters and \
|
102 |
+
isinstance(tool_def.function.parameters, dict) and \
|
103 |
+
"properties" in tool_def.function.parameters and \
|
104 |
+
isinstance(tool_def.function.parameters["properties"], dict) and \
|
105 |
+
param_name in tool_def.function.parameters["properties"] and \
|
106 |
+
isinstance(tool_def.function.parameters["properties"][param_name], dict):
|
107 |
+
target_type = tool_def.function.parameters["properties"][param_name].get("type")
|
108 |
+
break
|
109 |
+
|
110 |
+
typed_param_value = param_value_str # Default to string
|
111 |
+
if target_type:
|
112 |
+
try:
|
113 |
+
if target_type == "string":
|
114 |
+
typed_param_value = param_value_str
|
115 |
+
elif target_type == "integer":
|
116 |
+
typed_param_value = int(param_value_str)
|
117 |
+
elif target_type == "number":
|
118 |
+
typed_param_value = float(param_value_str)
|
119 |
+
elif target_type == "boolean":
|
120 |
+
typed_param_value = param_value_str.lower() == 'true'
|
121 |
+
elif target_type in ["object", "array"]:
|
122 |
+
try:
|
123 |
+
typed_param_value = json.loads(param_value_str)
|
124 |
+
except json.JSONDecodeError:
|
125 |
+
# Fallback for non-strict JSON like Python dict/list string
|
126 |
+
typed_param_value = ast.literal_eval(param_value_str)
|
127 |
+
else: # Unknown type, keep as string
|
128 |
+
typed_param_value = param_value_str
|
129 |
+
except (ValueError, SyntaxError, json.JSONDecodeError) as e:
|
130 |
+
logger.warning(
|
131 |
+
f"Could not convert param '{param_name}' with value '{param_value_str}' "
|
132 |
+
f"to type '{target_type}'. Error: {e}. Using string value."
|
133 |
+
)
|
134 |
+
typed_param_value = param_value_str
|
135 |
+
else: # No schema type, try ast.literal_eval
|
136 |
+
try:
|
137 |
+
# For values like "true", "123", "['a', 'b']"
|
138 |
+
# ast.literal_eval('some_string_without_quotes') will raise SyntaxError
|
139 |
+
if (param_value_str.startswith("'") and param_value_str.endswith("'")) or \
|
140 |
+
(param_value_str.startswith('"') and param_value_str.endswith('"')) or \
|
141 |
+
(param_value_str.startswith('[') and param_value_str.endswith(']')) or \
|
142 |
+
(param_value_str.startswith('{') and param_value_str.endswith('}')) or \
|
143 |
+
param_value_str.lower() in ['true', 'false', 'none'] or \
|
144 |
+
param_value_str.replace('.', '', 1).isdigit() or \
|
145 |
+
(param_value_str.startswith('-') and param_value_str[1:].replace('.', '', 1).isdigit()):
|
146 |
+
typed_param_value = ast.literal_eval(param_value_str)
|
147 |
+
else: # It's likely a plain string not meant for ast.literal_eval
|
148 |
+
typed_param_value = param_value_str
|
149 |
+
except (ValueError, SyntaxError):
|
150 |
+
typed_param_value = param_value_str # Keep as string if ast.literal_eval fails
|
151 |
+
|
152 |
+
parsed_arguments[param_name] = typed_param_value
|
153 |
+
|
154 |
+
parsed_tool_calls.append(ToolCall(
|
155 |
+
id=f"call_{random_uuid()}",
|
156 |
+
type="function",
|
157 |
+
function=FunctionCall(
|
158 |
+
name=tool_name,
|
159 |
+
arguments=json.dumps(parsed_arguments, ensure_ascii=False),
|
160 |
+
),
|
161 |
+
))
|
162 |
+
|
163 |
+
return ExtractedToolCallInformation(
|
164 |
+
tools_called=len(parsed_tool_calls) > 0,
|
165 |
+
tool_calls=parsed_tool_calls,
|
166 |
+
content=content if content else None,
|
167 |
+
)
|
168 |
+
|
169 |
+
except Exception:
|
170 |
+
logger.exception(f"Error in extracting XML tool call from response. Response: {model_output}")
|
171 |
+
# Fallback to original model output if parsing fails catastrophically
|
172 |
+
return ExtractedToolCallInformation(
|
173 |
+
tools_called=False,
|
174 |
+
tool_calls=[],
|
175 |
+
content=model_output,
|
176 |
+
)
|
177 |
+
|
178 |
+
def extract_tool_calls_streaming(
|
179 |
+
self,
|
180 |
+
previous_text: str,
|
181 |
+
current_text: str,
|
182 |
+
delta_text: str,
|
183 |
+
previous_token_ids: Sequence[int],
|
184 |
+
current_token_ids: Sequence[int],
|
185 |
+
delta_token_ids: Sequence[int],
|
186 |
+
request: ChatCompletionRequest,
|
187 |
+
) -> Union[DeltaMessage, None]:
|
188 |
+
|
189 |
+
raise NotImplementedError("Tool calling is not supported in streaming mode!")
|
190 |
+
|
191 |
+
|
192 |
+
@ToolParserManager.register_module("llama_nemotron_json")
|
193 |
+
class LlamaNemotronJSONToolParser(ToolParser):
|
194 |
+
|
195 |
+
def __init__(self, tokenizer: AnyTokenizer):
|
196 |
+
super().__init__(tokenizer)
|
197 |
+
|
198 |
+
self.current_tool_name_sent: bool = False
|
199 |
+
self.prev_tool_call_arr: list[dict] = []
|
200 |
+
self.current_tool_id: int = -1
|
201 |
+
self.streamed_args_for_tool: list[str] = []
|
202 |
+
|
203 |
+
self.tool_call_start_token: str = "<TOOLCALL>"
|
204 |
+
self.tool_call_end_token: str = "</TOOLCALL>"
|
205 |
+
|
206 |
+
self.tool_call_regex = re.compile(r"<TOOLCALL>(.*?)</TOOLCALL>", re.DOTALL)
|
207 |
+
|
208 |
+
def extract_tool_calls(
|
209 |
+
self,
|
210 |
+
model_output: str,
|
211 |
+
request: ChatCompletionRequest,
|
212 |
+
) -> ExtractedToolCallInformation:
|
213 |
+
|
214 |
+
if self.tool_call_start_token not in model_output:
|
215 |
+
return ExtractedToolCallInformation(
|
216 |
+
tools_called=False,
|
217 |
+
tool_calls=[],
|
218 |
+
content=model_output,
|
219 |
+
)
|
220 |
+
|
221 |
+
else:
|
222 |
+
|
223 |
+
try:
|
224 |
+
str_tool_calls = self.tool_call_regex.findall(model_output)[0].strip()
|
225 |
+
if not str_tool_calls.startswith("["):
|
226 |
+
str_tool_calls = "[" + str_tool_calls
|
227 |
+
if not str_tool_calls.endswith("]"):
|
228 |
+
str_tool_calls = "]" + str_tool_calls
|
229 |
+
json_tool_calls = json.loads(str_tool_calls)
|
230 |
+
tool_calls = []
|
231 |
+
for tool_call in json_tool_calls:
|
232 |
+
try:
|
233 |
+
tool_calls.append(ToolCall(
|
234 |
+
type="function",
|
235 |
+
function=FunctionCall(
|
236 |
+
name=tool_call["name"],
|
237 |
+
arguments=json.dumps(tool_call["arguments"], ensure_ascii=False) \
|
238 |
+
if isinstance(tool_call["arguments"], dict) else tool_call["arguments"],
|
239 |
+
),
|
240 |
+
))
|
241 |
+
except:
|
242 |
+
continue
|
243 |
+
|
244 |
+
content = model_output[:model_output.rfind(self.tool_call_start_token)]
|
245 |
+
|
246 |
+
return ExtractedToolCallInformation(
|
247 |
+
tools_called=True,
|
248 |
+
tool_calls=tool_calls,
|
249 |
+
content=content if content else None,
|
250 |
+
)
|
251 |
+
|
252 |
+
except Exception:
|
253 |
+
logger.exception(f"Error in extracting tool call from response. Response: {model_output}")
|
254 |
+
return ExtractedToolCallInformation(
|
255 |
+
tools_called=False,
|
256 |
+
tool_calls=[],
|
257 |
+
content=model_output,
|
258 |
+
)
|
259 |
+
|
260 |
+
def extract_tool_calls_streaming(
|
261 |
+
self,
|
262 |
+
previous_text: str,
|
263 |
+
current_text: str,
|
264 |
+
delta_text: str,
|
265 |
+
previous_token_ids: Sequence[int],
|
266 |
+
current_token_ids: Sequence[int],
|
267 |
+
delta_token_ids: Sequence[int],
|
268 |
+
request: ChatCompletionRequest,
|
269 |
+
) -> Union[DeltaMessage, None]:
|
270 |
+
|
271 |
+
raise NotImplementedError("Tool calling is not supported in streaming mode!")
|
272 |
+
|
273 |
+
|
274 |
+
@ToolParserManager.register_module("llama_nemotron_pythonic")
|
275 |
+
class LlamaNemotronPythonicToolParser(ToolParser):
|
276 |
+
|
277 |
+
def __init__(self, tokenizer: AnyTokenizer):
|
278 |
+
super().__init__(tokenizer)
|
279 |
+
|
280 |
+
self.current_tool_name_sent: bool = False
|
281 |
+
self.prev_tool_call_arr: list[dict] = []
|
282 |
+
self.current_tool_id: int = -1
|
283 |
+
self.streamed_args_for_tool: list[str] = []
|
284 |
+
|
285 |
+
self.tool_call_start_token: str = "<TOOLCALL>"
|
286 |
+
self.tool_call_end_token: str = "</TOOLCALL>"
|
287 |
+
|
288 |
+
self.tool_call_regex = re.compile(r"<TOOLCALL>(.*?)</TOOLCALL>", re.DOTALL)
|
289 |
+
# Regex to parse pythonic function calls: function_name(arg1="value1", arg2=123, arg3=True)
|
290 |
+
self.function_call_regex = re.compile(r"(\w+)\((.*?)\)$", re.DOTALL)
|
291 |
+
|
292 |
+
def parse_function_arguments(self, args_str: str) -> dict:
|
293 |
+
"""Parse pythonic function arguments string into a dictionary"""
|
294 |
+
if not args_str.strip():
|
295 |
+
return {}
|
296 |
+
|
297 |
+
# Use ast.parse to safely parse the function call arguments
|
298 |
+
# We'll construct a temporary function call and parse it
|
299 |
+
try:
|
300 |
+
# Create a dummy function call to parse arguments
|
301 |
+
dummy_code = f"dummy_func({args_str})"
|
302 |
+
parsed = ast.parse(dummy_code, mode='eval')
|
303 |
+
|
304 |
+
# Extract arguments from the AST
|
305 |
+
call_node = parsed.body
|
306 |
+
if not isinstance(call_node, ast.Call):
|
307 |
+
return {}
|
308 |
+
|
309 |
+
arguments = {}
|
310 |
+
|
311 |
+
# Handle keyword arguments
|
312 |
+
for keyword in call_node.keywords:
|
313 |
+
if keyword.arg is None: # **kwargs
|
314 |
+
continue
|
315 |
+
|
316 |
+
# Convert AST value to Python value
|
317 |
+
try:
|
318 |
+
value = ast.literal_eval(keyword.value)
|
319 |
+
arguments[keyword.arg] = value
|
320 |
+
except (ValueError, TypeError):
|
321 |
+
# If literal_eval fails, try to get the raw value
|
322 |
+
if isinstance(keyword.value, ast.Name):
|
323 |
+
arguments[keyword.arg] = keyword.value.id
|
324 |
+
elif isinstance(keyword.value, ast.Constant):
|
325 |
+
arguments[keyword.arg] = keyword.value.value
|
326 |
+
else:
|
327 |
+
# Fallback: convert to string
|
328 |
+
arguments[keyword.arg] = ast.unparse(keyword.value)
|
329 |
+
|
330 |
+
# Handle positional arguments (less common in tool calls but supported)
|
331 |
+
for i, arg in enumerate(call_node.args):
|
332 |
+
try:
|
333 |
+
value = ast.literal_eval(arg)
|
334 |
+
arguments[f"arg_{i}"] = value
|
335 |
+
except (ValueError, TypeError):
|
336 |
+
if isinstance(arg, ast.Name):
|
337 |
+
arguments[f"arg_{i}"] = arg.id
|
338 |
+
elif isinstance(arg, ast.Constant):
|
339 |
+
arguments[f"arg_{i}"] = arg.value
|
340 |
+
else:
|
341 |
+
arguments[f"arg_{i}"] = ast.unparse(arg)
|
342 |
+
|
343 |
+
return arguments
|
344 |
+
|
345 |
+
except (SyntaxError, ValueError) as e:
|
346 |
+
logger.warning(f"Failed to parse function arguments '{args_str}': {e}")
|
347 |
+
return {}
|
348 |
+
|
349 |
+
def extract_tool_calls(
|
350 |
+
self,
|
351 |
+
model_output: str,
|
352 |
+
request: ChatCompletionRequest,
|
353 |
+
) -> ExtractedToolCallInformation:
|
354 |
+
|
355 |
+
if self.tool_call_start_token not in model_output:
|
356 |
+
return ExtractedToolCallInformation(
|
357 |
+
tools_called=False,
|
358 |
+
tool_calls=[],
|
359 |
+
content=model_output,
|
360 |
+
)
|
361 |
+
|
362 |
+
tool_call_start_index = model_output.find(self.tool_call_start_token)
|
363 |
+
content = model_output[:tool_call_start_index].strip()
|
364 |
+
|
365 |
+
try:
|
366 |
+
# Extract content between <TOOLCALL> tags
|
367 |
+
tool_call_matches = self.tool_call_regex.findall(model_output)
|
368 |
+
if not tool_call_matches:
|
369 |
+
return ExtractedToolCallInformation(
|
370 |
+
tools_called=False,
|
371 |
+
tool_calls=[],
|
372 |
+
content=model_output,
|
373 |
+
)
|
374 |
+
|
375 |
+
tool_calls_content = tool_call_matches[0].strip()
|
376 |
+
|
377 |
+
# Split by lines to get individual function calls
|
378 |
+
function_lines = [line.strip() for line in tool_calls_content.split('\n') if line.strip()]
|
379 |
+
|
380 |
+
parsed_tool_calls = []
|
381 |
+
|
382 |
+
for func_line in function_lines:
|
383 |
+
# Parse each function call
|
384 |
+
match = self.function_call_regex.match(func_line)
|
385 |
+
if not match:
|
386 |
+
logger.warning(f"Could not parse function call: {func_line}")
|
387 |
+
continue
|
388 |
+
|
389 |
+
function_name = match.group(1)
|
390 |
+
args_str = match.group(2)
|
391 |
+
|
392 |
+
# Parse arguments
|
393 |
+
parsed_arguments = self.parse_function_arguments(args_str)
|
394 |
+
|
395 |
+
# Apply type conversion based on schema if available
|
396 |
+
if request.tools:
|
397 |
+
for tool_def in request.tools:
|
398 |
+
if tool_def.function.name == function_name:
|
399 |
+
schema_properties = {}
|
400 |
+
if (tool_def.function.parameters and
|
401 |
+
isinstance(tool_def.function.parameters, dict) and
|
402 |
+
"properties" in tool_def.function.parameters and
|
403 |
+
isinstance(tool_def.function.parameters["properties"], dict)):
|
404 |
+
schema_properties = tool_def.function.parameters["properties"]
|
405 |
+
|
406 |
+
# Convert arguments based on schema types
|
407 |
+
for arg_name, arg_value in parsed_arguments.items():
|
408 |
+
if arg_name in schema_properties:
|
409 |
+
param_info = schema_properties[arg_name]
|
410 |
+
target_type = param_info.get("type")
|
411 |
+
|
412 |
+
try:
|
413 |
+
if target_type == "string" and not isinstance(arg_value, str):
|
414 |
+
parsed_arguments[arg_name] = str(arg_value)
|
415 |
+
elif target_type == "integer" and not isinstance(arg_value, int):
|
416 |
+
parsed_arguments[arg_name] = int(arg_value)
|
417 |
+
elif target_type == "number" and not isinstance(arg_value, (int, float)):
|
418 |
+
parsed_arguments[arg_name] = float(arg_value)
|
419 |
+
elif target_type == "boolean" and not isinstance(arg_value, bool):
|
420 |
+
if isinstance(arg_value, str):
|
421 |
+
parsed_arguments[arg_name] = arg_value.lower() in ['true', '1', 'yes']
|
422 |
+
else:
|
423 |
+
parsed_arguments[arg_name] = bool(arg_value)
|
424 |
+
elif target_type in ["object", "array"]:
|
425 |
+
if isinstance(arg_value, str):
|
426 |
+
try:
|
427 |
+
parsed_arguments[arg_name] = json.loads(arg_value)
|
428 |
+
except json.JSONDecodeError:
|
429 |
+
# Keep as string if JSON parsing fails
|
430 |
+
pass
|
431 |
+
except (ValueError, TypeError) as e:
|
432 |
+
logger.warning(f"Type conversion failed for {arg_name}: {e}")
|
433 |
+
# Keep original value if conversion fails
|
434 |
+
break
|
435 |
+
|
436 |
+
parsed_tool_calls.append(ToolCall(
|
437 |
+
id=f"call_{random_uuid()}",
|
438 |
+
type="function",
|
439 |
+
function=FunctionCall(
|
440 |
+
name=function_name,
|
441 |
+
arguments=json.dumps(parsed_arguments, ensure_ascii=False),
|
442 |
+
),
|
443 |
+
))
|
444 |
+
|
445 |
+
return ExtractedToolCallInformation(
|
446 |
+
tools_called=len(parsed_tool_calls) > 0,
|
447 |
+
tool_calls=parsed_tool_calls,
|
448 |
+
content=content if content else None,
|
449 |
+
)
|
450 |
+
|
451 |
+
except Exception:
|
452 |
+
logger.exception(f"Error in extracting pythonic tool call from response. Response: {model_output}")
|
453 |
+
return ExtractedToolCallInformation(
|
454 |
+
tools_called=False,
|
455 |
+
tool_calls=[],
|
456 |
+
content=model_output,
|
457 |
+
)
|
458 |
+
|
459 |
+
def extract_tool_calls_streaming(
|
460 |
+
self,
|
461 |
+
previous_text: str,
|
462 |
+
current_text: str,
|
463 |
+
delta_text: str,
|
464 |
+
previous_token_ids: Sequence[int],
|
465 |
+
current_token_ids: Sequence[int],
|
466 |
+
delta_token_ids: Sequence[int],
|
467 |
+
request: ChatCompletionRequest,
|
468 |
+
) -> Union[DeltaMessage, None]:
|
469 |
+
|
470 |
+
raise NotImplementedError("Tool calling is not supported in streaming mode!")
|