[Rust Frontend] Align tool rendering for Kimi K3 (#50540)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Bugen Zhao committed
Aug 4, 2026 at 16:03 UTC
0b1c151cbcacd12e207f2a91e4ca9f5b6961ec8a
4 files changed
+85
-9
rust/src/chat/src/renderer/kimi_k3/encoding.rs
+9
-4
@@ -291,12 +291,17 @@ fn write_tool_declare(
291
let mut specs = Vec::with_capacity(tools.len());
292
for tool in tools {
293
let mut function = Map::new();
294
- function.insert(
295
- "description".to_string(),
296
- Value::String(tool.description.clone().unwrap_or_default()),
297
- );
294
+ if let Some(description) = &tool.description {
295
+ function.insert(
296
+ "description".to_string(),
297
+ Value::String(description.clone()),
298
+ );
299
+ }
300
function.insert("name".to_string(), Value::String(tool.name.clone()));
301
function.insert("parameters".to_string(), sort_json(&tool.parameters));
302
+ if let Some(strict) = tool.strict {
303
+ function.insert("strict".to_string(), Value::Bool(strict));
304
+ }
305
specs.push(json!({
306
"function": Value::Object(function),
307
"type": "function",
rust/src/chat/src/renderer/kimi_k3/fixtures/dynamic_system_tool_declare_input.json
+7
-2
@@ -5,6 +5,7 @@
5
"function": {
6
"name": "get_weather",
7
"description": "weather",
8
+ "strict": true,
9
"parameters": {
10
"type": "object",
11
"properties": {
@@ -34,12 +35,16 @@
35
"type": "function",
36
"function": {
37
"name": "calc",
37
- "description": "calculator",
38
+ "strict": false,
39
"parameters": {
40
"type": "object",
41
"properties": {
42
"x": {
42
- "type": "number"
43
+ "type": [
44
+ "number",
45
+ "null"
46
+ ],
47
+ "default": null
48
}
49
},
50
"required": [
rust/src/chat/src/renderer/kimi_k3/fixtures/dynamic_system_tool_declare_output.txt
+2
-2
@@ -2,7 +2,7 @@
2
Here are the available tools, described in JSONSchema.
3
4
```json
5
-[{"function":{"description":"weather","name":"get_weather","parameters":{"properties":{"city":{"type":"string"},"days":{"type":"number"}},"required":["city"],"type":"object"}},"type":"function"}]
5
+[{"function":{"description":"weather","name":"get_weather","parameters":{"properties":{"city":{"type":"string"},"days":{"type":"number"}},"required":["city"],"type":"object"},"strict":true},"type":"function"}]
6
```<|close|>message<|sep|><|end_of_msg|><|open|>message role="system" type="thinking-effort"<|sep|>`thinking_effort` guides on how much to think in your thinking channel (not including the response channel), supported values include `low`, `medium`, `high`, and `max`.
7
Now the system is invoked with `thinking_effort=max`.<|close|>message<|sep|><|end_of_msg|><|open|>message role="user"<|sep|>hi<|close|>message<|sep|><|end_of_msg|><|open|>message role="system" type="tool-declare"<|sep|>## New Tools Available
8
The system dynamically extends the toolset via lazy-loading.
@@ -10,5 +10,5 @@ You have access to all existing and extended tools.
10
Here are the specs for the extended tools.
11
12
```json
13
-[{"function":{"description":"calculator","name":"calc","parameters":{"properties":{"x":{"type":"number"}},"required":["x"],"type":"object"}},"type":"function"}]
13
+[{"function":{"name":"calc","parameters":{"properties":{"x":{"default":null,"type":["number","null"]}},"required":["x"],"type":"object"},"strict":false},"type":"function"}]
14
```<|close|>message<|sep|><|end_of_msg|><|open|>message role="user"<|sep|>use calc<|close|>message<|sep|><|end_of_msg|><|open|>message role="assistant"<|sep|><|open|>think<|sep|>
\ No newline at end of file
rust/src/chat/src/renderer/kimi_k3/tests.rs
+67
-1
@@ -17,7 +17,9 @@ use super::KimiK3ChatRenderer;
17
use crate::ChatRenderer;
18
use crate::renderer::kimi_k3::encoding::{CLOSE, END_OF_MSG, IMAGE_PLACEHOLDER, OPEN, SEP};
19
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
20
-use crate::request::{ChatContentPart, ChatMessage, GenerationPromptMode, ReasoningEffort};
20
+use crate::request::{
21
+ ChatContentPart, ChatMessage, ChatTool, GenerationPromptMode, ReasoningEffort,
22
+};
23
use crate::{AssistantContentBlock, AssistantToolCall};
24
25
const OPEN_ID: u32 = 256;
@@ -90,6 +92,70 @@ fn golden_dynamic_system_tool_declare() {
92
assert_golden("dynamic_system_tool_declare");
93
}
94
95
+#[test]
96
+fn tool_declare_omits_absent_fields_and_preserves_parameter_nulls() {
97
+ let mut request = crate::request::ChatRequest::for_test();
98
+ request.tools = vec![ChatTool {
99
+ name: "lookup".to_string(),
100
+ description: None,
101
+ parameters: json!({
102
+ "type": "object",
103
+ "properties": {
104
+ "limit": {
105
+ "type": ["integer", "null"],
106
+ "default": null
107
+ }
108
+ }
109
+ }),
110
+ strict: None,
111
+ }];
112
+
113
+ let rendered = render_request(&request);
114
+
115
+ assert!(rendered.contains(
116
+ r#"[{"function":{"name":"lookup","parameters":{"properties":{"limit":{"default":null,"type":["integer","null"]}},"type":"object"}},"type":"function"}]"#
117
+ ));
118
+ assert!(!rendered.contains(r#""description":"#));
119
+ assert!(!rendered.contains(r#""strict":"#));
120
+}
121
+
122
+#[test]
123
+fn tool_declare_preserves_present_optional_fields() {
124
+ let mut request = crate::request::ChatRequest::for_test();
125
+ request.tools = vec![ChatTool {
126
+ name: "lookup".to_string(),
127
+ description: Some("Look up a record".to_string()),
128
+ parameters: json!({"type": "object"}),
129
+ strict: Some(false),
130
+ }];
131
+
132
+ let rendered = render_request(&request);
133
+
134
+ assert!(rendered.contains(
135
+ r#"[{"function":{"description":"Look up a record","name":"lookup","parameters":{"type":"object"},"strict":false},"type":"function"}]"#
136
+ ));
137
+}
138
+
139
+#[test]
140
+fn malformed_tool_arguments_render_as_raw_text() {
141
+ let malformed = " {\"city\":\"Beijing\" ";
142
+ let mut request = crate::request::ChatRequest::for_test();
143
+ request.messages = vec![ChatMessage::assistant_blocks(vec![
144
+ AssistantContentBlock::ToolCall(AssistantToolCall {
145
+ id: "call-1".to_string(),
146
+ name: "weather".to_string(),
147
+ arguments: malformed.to_string(),
148
+ }),
149
+ ])];
150
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
151
+
152
+ let rendered = render_request(&request);
153
+
154
+ assert!(rendered.contains(&format!(
155
+ "<|open|>json type=\"object\"<|sep|>{malformed}<|close|>json<|sep|>"
156
+ )));
157
+}
158
+
159
#[test]
160
fn token_writer_protects_literal_control_and_media_markers() {
161
let tokenizer = Arc::new(test_tokenizer());