A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 1, 21:15 UTC
[Rust Frontend] Support include_reasoning=false (#44391)
Signed-off-by: RickyChen / 陳昭儒 <ricky.chen@infinirc.com>
Chao-Ju Chen committed
Jun 5, 2026 at 16:47 UTC
e64237ae82c9fa8abf9fabf96c2c4a9dcbfcd040
4 files changed
+544
-26
rust/src/server/src/routes/openai/chat_completions.rs
+348
-20
@@ -85,6 +85,7 @@ pub async fn chat_completions(
85
log_request,
86
prepared.include_usage,
87
prepared.requested_logprobs,
88
+ prepared.include_reasoning,
89
prepared.echo,
90
prepared.return_token_ids,
91
prepared.return_tokens_as_token_ids,
@@ -100,6 +101,7 @@ pub async fn chat_completions(
101
created,
102
prepared.requested_logprobs,
103
prepared.include_prompt_logprobs,
104
+ prepared.include_reasoning,
105
prepared.echo,
106
prepared.return_token_ids,
107
prepared.return_tokens_as_token_ids,
@@ -134,6 +136,7 @@ async fn collect_chat_completion(
136
created: u64,
137
requested_logprobs: bool,
138
include_prompt_logprobs: bool,
139
+ include_reasoning: bool,
140
echo: Option<String>,
141
return_token_ids: bool,
142
return_tokens_as_token_ids: bool,
@@ -157,6 +160,11 @@ async fn collect_chat_completion(
160
} = collected;
161
let stop_reason = finish_reason.as_stop_reason().map(stop_reason_to_json);
162
let saw_tool_calls = message.tool_calls().next().is_some();
163
+ let reasoning = message.reasoning();
164
+ // Output logprobs and token IDs cover the complete generated token stream.
165
+ // When reasoning is hidden, omit them rather than leaking hidden reasoning
166
+ // tokens through per-token metadata.
167
+ let include_output_metadata = include_reasoning || reasoning.is_none();
168
let finish_reason = chat_finish_reason_to_openai(&finish_reason, saw_tool_calls)?.to_string();
169
let tool_calls = message
170
.tool_calls()
@@ -169,7 +177,7 @@ async fn collect_chat_completion(
177
},
178
})
179
.collect::<Vec<_>>();
172
- let logprobs = if requested_logprobs {
180
+ let logprobs = if requested_logprobs && include_output_metadata {
181
Some(decoded_logprobs_to_openai_chat(
182
logprobs.as_ref().ok_or_else(|| {
183
server_error!("chat response requested logprobs but generation returned none")
@@ -207,12 +215,12 @@ async fn collect_chat_completion(
215
None => Some(message.text()).filter(|t| !t.is_empty()),
216
},
217
tool_calls: Some(tool_calls).filter(|calls| !calls.is_empty()),
210
- reasoning: message.reasoning(),
218
+ reasoning: if include_reasoning { reasoning } else { None },
219
},
220
logprobs,
221
finish_reason: Some(finish_reason),
222
stop_reason,
215
- token_ids: return_token_ids.then_some(token_ids),
223
+ token_ids: (return_token_ids && include_output_metadata).then_some(token_ids),
224
}],
225
usage: Some(usage),
226
system_fingerprint: None,
@@ -232,12 +240,18 @@ async fn chat_completion_chunk_stream(
240
log_request: bool,
241
include_usage: bool,
242
requested_logprobs: bool,
243
+ include_reasoning: bool,
244
echo: Option<String>,
245
return_token_ids: bool,
246
return_tokens_as_token_ids: bool,
247
mut y: TryYielder<ChatCompletionStreamResponse, ApiError>,
248
) -> Result<(), ApiError> {
249
let mut saw_tool_calls = false;
250
+ // `LogprobsDelta` is emitted after all chat events for one decoded update.
251
+ // If that update contains hidden reasoning, including delimiter-only block
252
+ // starts or ends, omit its token metadata as well as its visible delta.
253
+ let mut inside_hidden_reasoning = false;
254
+ let mut suppress_current_update_metadata = false;
255
256
// If the client requested logprobs or token_ids, we need to buffer chunks until
257
// we receive the separate `LogprobsDelta` event, so that we can emit one
@@ -268,29 +282,44 @@ async fn chat_completion_chunk_stream(
282
}
283
}
284
Ok(ChatEvent::BlockDelta { kind, delta, .. }) => {
271
- if let Some(pending_chunk) = pending_chunk.as_mut() {
272
- pending_chunk.push_block_delta(kind, delta);
285
+ let include_delta =
286
+ include_reasoning || !matches!(kind, AssistantBlockKind::Reasoning);
287
+ if include_delta {
288
+ if let Some(pending_chunk) = pending_chunk.as_mut() {
289
+ pending_chunk.push_block_delta(kind, delta);
290
+ } else {
291
+ y.yield_ok(block_delta_chunk(
292
+ &request_id,
293
+ &response_model,
294
+ created,
295
+ kind,
296
+ delta,
297
+ ))
298
+ .await;
299
+ }
300
} else {
274
- y.yield_ok(block_delta_chunk(
275
- &request_id,
276
- &response_model,
277
- created,
278
- kind,
279
- delta,
280
- ))
281
- .await;
301
+ suppress_current_update_metadata = true;
302
}
303
}
304
Ok(ChatEvent::LogprobsDelta {
305
logprobs,
306
token_ids,
307
}) => {
288
- let openai_logprobs = logprobs
289
- .as_ref()
290
- .map(|lp| decoded_logprobs_to_openai_chat(lp, return_tokens_as_token_ids))
291
- .transpose()?;
292
- let openai_token_ids =
293
- return_token_ids.then_some(token_ids).filter(|t| !t.is_empty());
308
+ let include_metadata =
309
+ !suppress_current_update_metadata && !inside_hidden_reasoning;
310
+ suppress_current_update_metadata = false;
311
+ let openai_logprobs = if include_metadata {
312
+ logprobs
313
+ .as_ref()
314
+ .map(|lp| decoded_logprobs_to_openai_chat(lp, return_tokens_as_token_ids))
315
+ .transpose()?
316
+ } else {
317
+ None
318
+ };
319
+ let openai_token_ids = include_metadata
320
+ .then_some(token_ids)
321
+ .and_then(|token_ids| return_token_ids.then_some(token_ids))
322
+ .filter(|t| !t.is_empty());
323
if let Some(pending_chunk) = pending_chunk.as_mut() {
324
pending_chunk.logprobs = openai_logprobs;
325
pending_chunk.token_ids = openai_token_ids;
@@ -311,9 +340,17 @@ async fn chat_completion_chunk_stream(
340
}
341
Ok(ChatEvent::BlockStart { kind, .. }) => {
342
debug!(?kind, "starting new block");
343
+ if !include_reasoning && matches!(kind, AssistantBlockKind::Reasoning) {
344
+ inside_hidden_reasoning = true;
345
+ suppress_current_update_metadata = true;
346
+ }
347
}
348
Ok(ChatEvent::BlockEnd { .. }) => {
349
debug!("ending current block");
350
+ if inside_hidden_reasoning {
351
+ inside_hidden_reasoning = false;
352
+ suppress_current_update_metadata = true;
353
+ }
354
}
355
Ok(ChatEvent::ToolCallStart { index, id, name }) => {
356
let tool_index = index as u32;
@@ -763,7 +800,9 @@ fn stop_reason_to_json(stop_reason: &StopReason) -> Value {
800
mod tests {
801
use futures::{StreamExt as _, stream};
802
use serde_json::json;
766
- use vllm_chat::{AssistantBlockKind, AssistantToolCall, ChatEvent, FinishReason};
803
+ use vllm_chat::{
804
+ AssistantBlockKind, AssistantContentBlock, AssistantToolCall, ChatEvent, FinishReason,
805
+ };
806
use vllm_engine_core_client::protocol::StopReason;
807
use vllm_text::{DecodedLogprobs, DecodedPositionLogprobs, DecodedTokenLogprob};
808
@@ -895,6 +934,7 @@ mod tests {
934
false,
935
false,
936
true,
937
+ true,
938
None,
939
false,
940
false,
@@ -958,6 +998,7 @@ mod tests {
998
false,
999
false,
1000
true,
1001
+ true,
1002
None,
1003
false,
1004
false,
@@ -976,6 +1017,292 @@ mod tests {
1017
assert!(chunks[1].choices[0].logprobs.is_some());
1018
}
1019
1020
+ #[tokio::test]
1021
+ async fn chunk_stream_omits_reasoning_delta_when_disabled() {
1022
+ let stream = stream::iter(vec![
1023
+ Ok(ChatEvent::Start {
1024
+ prompt_token_ids: vec![].into(),
1025
+ prompt_logprobs: None,
1026
+ }),
1027
+ Ok(ChatEvent::BlockDelta {
1028
+ index: 0,
1029
+ kind: AssistantBlockKind::Reasoning,
1030
+ delta: "think".to_string(),
1031
+ }),
1032
+ Ok(ChatEvent::BlockDelta {
1033
+ index: 1,
1034
+ kind: AssistantBlockKind::Text,
1035
+ delta: "answer".to_string(),
1036
+ }),
1037
+ Ok(ChatEvent::Done {
1038
+ message: Default::default(),
1039
+ prompt_token_count: 1,
1040
+ output_token_count: 2,
1041
+ finish_reason: FinishReason::stop_eos(),
1042
+ kv_transfer_params: None,
1043
+ }),
1044
+ ]);
1045
+
1046
+ let chunks = chat_completion_chunk_stream(
1047
+ stream,
1048
+ "chatcmpl-1".to_string(),
1049
+ "model".to_string(),
1050
+ 1,
1051
+ false,
1052
+ false,
1053
+ false,
1054
+ false,
1055
+ None,
1056
+ false,
1057
+ false,
1058
+ )
1059
+ .collect::<Vec<_>>()
1060
+ .await
1061
+ .into_iter()
1062
+ .collect::<Result<Vec<_>, _>>()
1063
+ .expect("stream chunks");
1064
+
1065
+ assert_eq!(chunks.len(), 3);
1066
+ assert_eq!(
1067
+ chunks[1].choices[0].delta.content.as_deref(),
1068
+ Some("answer")
1069
+ );
1070
+ assert!(
1071
+ chunks
1072
+ .iter()
1073
+ .all(|chunk| chunk.choices.iter().all(|choice| choice.delta.reasoning.is_none()))
1074
+ );
1075
+ }
1076
+
1077
+ #[tokio::test]
1078
+ async fn chunk_stream_omits_logprobs_for_suppressed_reasoning() {
1079
+ let stream = stream::iter(vec![
1080
+ Ok(ChatEvent::Start {
1081
+ prompt_token_ids: vec![].into(),
1082
+ prompt_logprobs: None,
1083
+ }),
1084
+ Ok(ChatEvent::BlockDelta {
1085
+ index: 0,
1086
+ kind: AssistantBlockKind::Reasoning,
1087
+ delta: "think".to_string(),
1088
+ }),
1089
+ Ok(ChatEvent::LogprobsDelta {
1090
+ logprobs: Some(DecodedLogprobs {
1091
+ positions: vec![DecodedPositionLogprobs {
1092
+ entries: vec![DecodedTokenLogprob {
1093
+ token_id: 11,
1094
+ token: "think".to_string(),
1095
+ logprob: -0.1,
1096
+ rank: 1,
1097
+ }],
1098
+ }],
1099
+ }),
1100
+ token_ids: vec![11],
1101
+ }),
1102
+ Ok(ChatEvent::BlockDelta {
1103
+ index: 1,
1104
+ kind: AssistantBlockKind::Text,
1105
+ delta: "answer".to_string(),
1106
+ }),
1107
+ Ok(ChatEvent::LogprobsDelta {
1108
+ logprobs: Some(DecodedLogprobs {
1109
+ positions: vec![DecodedPositionLogprobs {
1110
+ entries: vec![DecodedTokenLogprob {
1111
+ token_id: 22,
1112
+ token: "answer".to_string(),
1113
+ logprob: -0.2,
1114
+ rank: 1,
1115
+ }],
1116
+ }],
1117
+ }),
1118
+ token_ids: vec![22],
1119
+ }),
1120
+ Ok(ChatEvent::Done {
1121
+ message: Default::default(),
1122
+ prompt_token_count: 1,
1123
+ output_token_count: 2,
1124
+ finish_reason: FinishReason::stop_eos(),
1125
+ kv_transfer_params: None,
1126
+ }),
1127
+ ]);
1128
+
1129
+ let chunks = chat_completion_chunk_stream(
1130
+ stream,
1131
+ "chatcmpl-1".to_string(),
1132
+ "model".to_string(),
1133
+ 1,
1134
+ false,
1135
+ false,
1136
+ true,
1137
+ false,
1138
+ None,
1139
+ true,
1140
+ false,
1141
+ )
1142
+ .collect::<Vec<_>>()
1143
+ .await
1144
+ .into_iter()
1145
+ .collect::<Result<Vec<_>, _>>()
1146
+ .expect("stream chunks");
1147
+
1148
+ assert_eq!(chunks.len(), 3);
1149
+ let choice = &chunks[1].choices[0];
1150
+ assert_eq!(choice.delta.content.as_deref(), Some("answer"));
1151
+ assert_eq!(choice.token_ids.as_deref(), Some(&[22][..]));
1152
+ let logprobs = choice.logprobs.as_ref().expect("answer logprobs");
1153
+ let content = logprobs.content.as_ref().expect("logprobs content");
1154
+ assert_eq!(content[0].token, "answer");
1155
+ assert!(chunks.iter().all(|chunk| {
1156
+ chunk.choices.iter().all(|choice| {
1157
+ choice.delta.reasoning.is_none()
1158
+ && choice.token_ids.as_deref() != Some(&[11][..])
1159
+ && choice
1160
+ .logprobs
1161
+ .as_ref()
1162
+ .and_then(|logprobs| logprobs.content.as_ref())
1163
+ .is_none_or(|content| content.iter().all(|entry| entry.token != "think"))
1164
+ })
1165
+ }));
1166
+ }
1167
+
1168
+ #[tokio::test]
1169
+ async fn chunk_stream_omits_logprobs_for_hidden_reasoning_delimiters() {
1170
+ let stream = stream::iter(vec![
1171
+ Ok(ChatEvent::Start {
1172
+ prompt_token_ids: vec![].into(),
1173
+ prompt_logprobs: None,
1174
+ }),
1175
+ Ok(ChatEvent::BlockStart {
1176
+ index: 0,
1177
+ kind: AssistantBlockKind::Reasoning,
1178
+ }),
1179
+ Ok(ChatEvent::LogprobsDelta {
1180
+ logprobs: Some(DecodedLogprobs {
1181
+ positions: vec![DecodedPositionLogprobs {
1182
+ entries: vec![DecodedTokenLogprob {
1183
+ token_id: 11,
1184
+ token: "<think>".to_string(),
1185
+ logprob: -0.1,
1186
+ rank: 1,
1187
+ }],
1188
+ }],
1189
+ }),
1190
+ token_ids: vec![11],
1191
+ }),
1192
+ Ok(ChatEvent::BlockDelta {
1193
+ index: 0,
1194
+ kind: AssistantBlockKind::Reasoning,
1195
+ delta: "think".to_string(),
1196
+ }),
1197
+ Ok(ChatEvent::LogprobsDelta {
1198
+ logprobs: Some(DecodedLogprobs {
1199
+ positions: vec![DecodedPositionLogprobs {
1200
+ entries: vec![DecodedTokenLogprob {
1201
+ token_id: 12,
1202
+ token: "think".to_string(),
1203
+ logprob: -0.2,
1204
+ rank: 1,
1205
+ }],
1206
+ }],
1207
+ }),
1208
+ token_ids: vec![12],
1209
+ }),
1210
+ Ok(ChatEvent::BlockEnd {
1211
+ index: 0,
1212
+ block: AssistantContentBlock::Reasoning {
1213
+ text: "think".to_string(),
1214
+ },
1215
+ }),
1216
+ Ok(ChatEvent::LogprobsDelta {
1217
+ logprobs: Some(DecodedLogprobs {
1218
+ positions: vec![DecodedPositionLogprobs {
1219
+ entries: vec![DecodedTokenLogprob {
1220
+ token_id: 13,
1221
+ token: "</think>".to_string(),
1222
+ logprob: -0.3,
1223
+ rank: 1,
1224
+ }],
1225
+ }],
1226
+ }),
1227
+ token_ids: vec![13],
1228
+ }),
1229
+ Ok(ChatEvent::BlockStart {
1230
+ index: 1,
1231
+ kind: AssistantBlockKind::Text,
1232
+ }),
1233
+ Ok(ChatEvent::BlockDelta {
1234
+ index: 1,
1235
+ kind: AssistantBlockKind::Text,
1236
+ delta: "answer".to_string(),
1237
+ }),
1238
+ Ok(ChatEvent::LogprobsDelta {
1239
+ logprobs: Some(DecodedLogprobs {
1240
+ positions: vec![DecodedPositionLogprobs {
1241
+ entries: vec![DecodedTokenLogprob {
1242
+ token_id: 22,
1243
+ token: "answer".to_string(),
1244
+ logprob: -0.4,
1245
+ rank: 1,
1246
+ }],
1247
+ }],
1248
+ }),
1249
+ token_ids: vec![22],
1250
+ }),
1251
+ Ok(ChatEvent::Done {
1252
+ message: Default::default(),
1253
+ prompt_token_count: 1,
1254
+ output_token_count: 4,
1255
+ finish_reason: FinishReason::stop_eos(),
1256
+ kv_transfer_params: None,
1257
+ }),
1258
+ ]);
1259
+
1260
+ let chunks = chat_completion_chunk_stream(
1261
+ stream,
1262
+ "chatcmpl-1".to_string(),
1263
+ "model".to_string(),
1264
+ 1,
1265
+ false,
1266
+ false,
1267
+ true,
1268
+ false,
1269
+ None,
1270
+ true,
1271
+ false,
1272
+ )
1273
+ .collect::<Vec<_>>()
1274
+ .await
1275
+ .into_iter()
1276
+ .collect::<Result<Vec<_>, _>>()
1277
+ .expect("stream chunks");
1278
+
1279
+ assert_eq!(chunks.len(), 3);
1280
+ let choice = &chunks[1].choices[0];
1281
+ assert_eq!(choice.delta.content.as_deref(), Some("answer"));
1282
+ assert_eq!(choice.token_ids.as_deref(), Some(&[22][..]));
1283
+ let logprobs = choice.logprobs.as_ref().expect("answer logprobs");
1284
+ let content = logprobs.content.as_ref().expect("logprobs content");
1285
+ assert_eq!(content[0].token, "answer");
1286
+ assert!(chunks.iter().all(|chunk| {
1287
+ chunk.choices.iter().all(|choice| {
1288
+ choice.delta.reasoning.is_none()
1289
+ && !choice
1290
+ .token_ids
1291
+ .as_ref()
1292
+ .is_some_and(|ids| matches!(ids.as_slice(), [11] | [12] | [13]))
1293
+ && choice
1294
+ .logprobs
1295
+ .as_ref()
1296
+ .and_then(|logprobs| logprobs.content.as_ref())
1297
+ .is_none_or(|content| {
1298
+ content.iter().all(|entry| {
1299
+ !matches!(entry.token.as_str(), "<think>" | "think" | "</think>")
1300
+ })
1301
+ })
1302
+ })
1303
+ }));
1304
+ }
1305
+
1306
#[tokio::test]
1307
async fn chunk_stream_preserves_tool_call_index_and_omits_id_from_arguments_delta() {
1308
let stream = stream::iter(vec![
@@ -1017,6 +1344,7 @@ mod tests {
1344
false,
1345
false,
1346
false,
1347
+ true,
1348
None,
1349
false,
1350
false,
rust/src/server/src/routes/openai/chat_completions/convert.rs
+21
@@ -29,6 +29,8 @@ pub struct PreparedRequest {
29
pub requested_logprobs: bool,
30
/// Whether the caller requested top-level prompt logprobs.
31
pub include_prompt_logprobs: bool,
32
+ /// Whether to include reasoning content in OpenAI responses.
33
+ pub include_reasoning: bool,
34
/// Lowered chat request for `vllm-chat`.
35
pub chat_request: ChatRequest,
36
/// Last assistant-role message content to echo back when `echo=true`.
@@ -57,6 +59,7 @@ pub(crate) fn prepare_chat_request(
59
.as_ref()
60
.map(|request| request.lora_name.clone())
61
.unwrap_or_else(|| lora_resolution.model_names.first().cloned().unwrap_or_default());
62
+ let include_reasoning = request.include_reasoning;
63
let echo = request
64
.echo
65
.then(|| extract_last_assistant_content(&request.messages))
@@ -146,6 +149,7 @@ pub(crate) fn prepare_chat_request(
149
include_usage,
150
requested_logprobs,
151
include_prompt_logprobs,
152
+ include_reasoning,
153
chat_request,
154
echo,
155
return_token_ids: request.return_token_ids.unwrap_or(false),
@@ -480,6 +484,23 @@ mod tests {
484
assert_eq!(prepared.chat_request.tool_choice, ChatToolChoice::Auto);
485
}
486
487
+ #[test]
488
+ fn prepare_chat_request_preserves_include_reasoning_false() {
489
+ let request = ChatCompletionRequest {
490
+ include_reasoning: false,
491
+ ..base_request()
492
+ };
493
+
494
+ let prepared = prepare_chat_request(
495
+ request,
496
+ &served(&["Qwen/Qwen1.5-0.5B-Chat"]),
497
+ ResolvedRequestContext::default(),
498
+ )
499
+ .expect("request is valid");
500
+
501
+ assert!(!prepared.include_reasoning);
502
+ }
503
+
504
#[test]
505
fn prepare_chat_request_preserves_sampling_passthrough_fields() {
506
let request = ChatCompletionRequest {
rust/src/server/src/routes/openai/chat_completions/validate.rs
+11
-6
@@ -120,12 +120,6 @@ pub(super) fn validate_request_compat(
120
"thinking_token_budget",
121
"thinking_token_budget is not supported.",
122
)?;
123
- if !request.include_reasoning {
124
- bail_invalid_request!(
125
- param = "include_reasoning",
126
- "include_reasoning is not supported."
127
- );
128
- }
123
reject_non_default(
124
request.media_io_kwargs.as_ref(),
125
"media_io_kwargs",
@@ -312,6 +306,17 @@ mod tests {
306
.expect("reasoning_effort should be accepted");
307
}
308
309
+ #[test]
310
+ fn validate_request_compat_accepts_include_reasoning_false() {
311
+ let request = ChatCompletionRequest {
312
+ include_reasoning: false,
313
+ ..base_request()
314
+ };
315
+
316
+ validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
317
+ .expect("include_reasoning=false should be accepted");
318
+ }
319
+
320
#[test]
321
fn validate_request_compat_rejects_top_logprobs_without_logprobs() {
322
let request = ChatCompletionRequest {
rust/src/server/src/routes/tests.rs
+164
@@ -3492,6 +3492,170 @@ async fn reasoning_blocks_are_mapped_to_reasoning_sse_chunks() {
3492
assert!(text.contains("\"content\":\"answer\""), "{text}");
3493
}
3494
3495
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3496
+#[serial]
3497
+async fn include_reasoning_false_suppresses_reasoning_in_non_stream_chat() {
3498
+ let (app, engine_task) = test_app_with_backend_and_stream_output_specs(
3499
+ Arc::new(FakeChatBackend::with_model_id("Qwen/Qwen3-0.6B")),
3500
+ vec![
3501
+ (bytes_to_token_ids(b"<think>think</think>"), None),
3502
+ (
3503
+ bytes_to_token_ids(b"answer"),
3504
+ Some(EngineCoreFinishReason::Length),
3505
+ ),
3506
+ ],
3507
+ )
3508
+ .await;
3509
+
3510
+ let response = app
3511
+ .clone()
3512
+ .call(
3513
+ Request::builder()
3514
+ .method("POST")
3515
+ .uri("/v1/chat/completions")
3516
+ .header("content-type", "application/json")
3517
+ .body(Body::from(
3518
+ json!({
3519
+ "model": "Qwen/Qwen1.5-0.5B-Chat",
3520
+ "stream": false,
3521
+ "include_reasoning": false,
3522
+ "messages": [{"role": "user", "content": "hello"}]
3523
+ })
3524
+ .to_string(),
3525
+ ))
3526
+ .expect("build request"),
3527
+ )
3528
+ .await
3529
+ .expect("call app");
3530
+
3531
+ assert_eq!(response.status(), StatusCode::OK);
3532
+ let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
3533
+ engine_task.await.expect("mock engine task");
3534
+ let text = String::from_utf8(body.to_vec()).expect("utf8 body");
3535
+ let json: serde_json::Value = serde_json::from_str(&text).expect("decode json");
3536
+
3537
+ assert_eq!(json["choices"][0]["message"]["content"], "answer");
3538
+ assert!(
3539
+ json["choices"][0]["message"]
3540
+ .as_object()
3541
+ .is_some_and(|message| !message.contains_key("reasoning")),
3542
+ "{text}"
3543
+ );
3544
+}
3545
+
3546
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3547
+#[serial]
3548
+async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
3549
+ let ipc = IpcNamespace::new().expect("create ipc namespace");
3550
+ let handshake_address = ipc.handshake_endpoint();
3551
+ let engine_id = b"engine-openai-hidden-reasoning-logprobs".to_vec();
3552
+
3553
+ let engine_task = MockEngineTask::new(spawn_mock_engine_task(
3554
+ handshake_address.clone(),
3555
+ engine_id.clone(),
3556
+ |dealer, push| {
3557
+ boxed_test_future(async move {
3558
+ let add = recv_engine_message(dealer).await;
3559
+ let request: EngineCoreRequest =
3560
+ rmp_serde::from_slice(&add[1]).expect("decode request");
3561
+ let reasoning_token_ids = bytes_to_token_ids(b"<think>think</think>");
3562
+ let answer_token_ids = bytes_to_token_ids(b"answer");
3563
+
3564
+ send_outputs(
3565
+ push,
3566
+ EngineCoreOutputs {
3567
+ engine_index: 0,
3568
+ outputs: vec![
3569
+ request_output_with_logprobs(
3570
+ &request.request_id,
3571
+ reasoning_token_ids.clone(),
3572
+ None,
3573
+ None,
3574
+ Some(sample_logprobs_for_tokens(&reasoning_token_ids)),
3575
+ None,
3576
+ ),
3577
+ request_output_with_logprobs(
3578
+ &request.request_id,
3579
+ answer_token_ids.clone(),
3580
+ Some(EngineCoreFinishReason::Length),
3581
+ None,
3582
+ Some(sample_logprobs_for_tokens(&answer_token_ids)),
3583
+ None,
3584
+ ),
3585
+ ],
3586
+ scheduler_stats: None,
3587
+ timestamp: 0.0,
3588
+ utility_output: None,
3589
+ finished_requests: None,
3590
+ wave_complete: None,
3591
+ start_wave: None,
3592
+ },
3593
+ )
3594
+ .await;
3595
+ })
3596
+ },
3597
+ ));
3598
+
3599
+ let client = EngineCoreClient::connect(
3600
+ EngineCoreClientConfig::new_single(handshake_address)
3601
+ .with_model_name("test-model")
3602
+ .with_local_input_output_addresses(
3603
+ Some(ipc.input_endpoint()),
3604
+ Some(ipc.output_endpoint()),
3605
+ ),
3606
+ )
3607
+ .await
3608
+ .expect("connect client");
3609
+ let chat = ChatLlm::from_shared_backend(
3610
+ test_llm(client),
3611
+ Arc::new(FakeChatBackend::with_model_id("Qwen/Qwen3-0.6B")),
3612
+ );
3613
+ let mut app = build_router(Arc::new(AppState::new(
3614
+ vec!["Qwen/Qwen1.5-0.5B-Chat".to_string()],
3615
+ chat,
3616
+ )));
3617
+
3618
+ let response = app
3619
+ .call(
3620
+ Request::builder()
3621
+ .method("POST")
3622
+ .uri("/v1/chat/completions")
3623
+ .header("content-type", "application/json")
3624
+ .body(Body::from(
3625
+ json!({
3626
+ "model": "Qwen/Qwen1.5-0.5B-Chat",
3627
+ "stream": false,
3628
+ "include_reasoning": false,
3629
+ "logprobs": true,
3630
+ "return_token_ids": true,
3631
+ "messages": [{"role": "user", "content": "hello"}]
3632
+ })
3633
+ .to_string(),
3634
+ ))
3635
+ .expect("build request"),
3636
+ )
3637
+ .await
3638
+ .expect("call app");
3639
+
3640
+ assert_eq!(response.status(), StatusCode::OK);
3641
+ let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
3642
+ engine_task.await.expect("mock engine task");
3643
+ let text = String::from_utf8(body.to_vec()).expect("utf8 body");
3644
+ let json: serde_json::Value = serde_json::from_str(&text).expect("decode json");
3645
+ let choice = json["choices"][0].as_object().expect("choice object");
3646
+
3647
+ assert_eq!(json["choices"][0]["message"]["content"], "answer");
3648
+ assert!(
3649
+ json["choices"][0]["message"]
3650
+ .as_object()
3651
+ .is_some_and(|message| !message.contains_key("reasoning")),
3652
+ "{text}"
3653
+ );
3654
+ assert!(!choice.contains_key("logprobs"), "{text}");
3655
+ assert!(!choice.contains_key("token_ids"), "{text}");
3656
+ assert!(json["prompt_token_ids"].is_array(), "{text}");
3657
+}
3658
+
3659
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3660
#[serial]
3661
async fn tool_calls_are_mapped_to_tool_call_sse_chunks() {