Please do a quick search on GitHub issues first, the feature you are about to request might have already been requested.
Expected Behavior ChatModel.stream(xxx) .flatMapSequential(f -> { System.out.println(f.getResult().getOutput().getContent()); // Output reasoning_content System.out.println(f.getResult().getOutput().getReasoningContent()); })
Current Behavior
ChatModel.stream(xxx) .flatMapSequential(f -> { // Output reasoning_content is not supported System.out.println(f.getResult().getOutput().getContent()); })
Context
When launching LLM Q&A using ChatModel, the thinking response of deepseek-r1 will be output in reasoning_content. Currently, there is only content field in Message output. Unable to receive LLM's thinking, want to add related fields.
Comment From: Ltyro
+1,怎么还不支持思维链,烦死了
Comment From: dev-jonghoonpark
related document : https://api-docs.deepseek.com/guides/reasoning_model
Comment From: hardikSinghBehl
Currently, the chain of thought (reasoning_content) is merged with the actual content, which also causes the structured output converters to fail. I created the rudimentary custom converter below as a temporary workaround, but yes, this functionality should be natively supported by the library.
DeepSeekModelOutputConverter.java ChatbotService.java
Comment From: apappascs
This issue is getting resolved by this PR https://github.com/spring-projects/spring-ai/pull/2192
Example of deepseek-reasoner call and response:
request:
curl -v -X POST "https://api.deepseek.com/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-api-key" \
-d '{
"model": "deepseek-reasoner",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the answer to the Great Question of Life, the Universe, and Everything?"}
],
"stream": false
}'
response:
{
"id":"6090f86a-12aa-4xxxx-89af-85xxxxxx",
"object":"chat.completion",
"created":1740134353,
"model":"deepseek-reasoner",
"choices":[
{
"index":0,
"message":{
"role":"assistant",
"content":"The answer to the Great Question of Life, the Universe, and Everything, as famously depicted in Douglas Adams' *The Hitchhiker's Guide to the Galaxy*, is **42**. \n\nHowever, the story humorously reveals that while the supercomputer Deep Thought calculated this answer over millions of years, the actual *question* corresponding to it remains ambiguous—highlighting the absurdity of seeking absolute meaning in a vast, chaotic universe. 😊",
"reasoning_content":"Okay, let's see. The user is asking about the answer to the Great Question of Life, the Universe, and Everything. Hmm, I remember that this is a reference to \"The Hitchhiker's Guide to the Galaxy\" by Douglas Adams. In the book, a supercomputer named Deep Thought was built to calculate the answer to this ultimate question. After a lot of time and processing, the computer comes up with the number 42. But then the characters realize they didn't actually know what the question was. So the answer is 42, but the joke is that the question isn't really known.\n\nWait, but maybe the user is looking for a more philosophical answer? Like, not just the fictional reference. But given the way the question is phrased, \"the Great Question of Life, the Universe, and Everything\" is almost certainly pointing to the Hitchhiker's Guide joke. The answer is famously 42. I should confirm that I'm not missing any other context here. Maybe check if there's another interpretation, but I don't think so. This is a well-known pop culture reference. So the answer is 42, and maybe a brief explanation about the book reference to be helpful."
},
"logprobs":null,
"finish_reason":"stop"
}
],
"usage":{
"prompt_tokens":28,
"completion_tokens":338,
"total_tokens":366,
"prompt_tokens_details":{
"cached_tokens":0
},
"completion_tokens_details":{
"reasoning_tokens":247
},
"prompt_cache_hit_tokens":0,
"prompt_cache_miss_tokens":28
},
"system_fingerprint":"fp_5417b77867_prod"
}
Comment From: KonngExplorer
@Ltyro I have an alternative solution:
We can set up a WebClient interceptor to redirect the reasoning_content
field from the DeepSeek official API into the content
field, wrapping it with <think></think>
tags. Afterwards, we can parse the chain-of-thought content from the content
field. This temporarily resolves the issue of the current Spring-AI version not supporting the retrieval of the reasoning_content
field.
@Slf4j
@Configuration
public class WebClientConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
WebClientCustomizer webClientCustomizer() {
return webClientBuilder -> {
ExchangeFilterFunction requestFilter = ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
log.info("Request Method: {}", clientRequest.method());
log.info("Request URL: {}", clientRequest.url());
return Mono.just(clientRequest);
});
// 响应拦截器
ExchangeFilterFunction responseFilter = ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.info("Intercepted Response Status: {}", clientResponse.statusCode());
final boolean[] thinking = {false};
// 处理SSE事件流
Flux<DataBuffer> modifiedBody = clientResponse.bodyToFlux(DataBuffer.class)
.map(buf -> {
byte[] bytes = new byte[buf.readableByteCount()];
buf.read(bytes);
DataBufferUtils.release(buf);
return new String(bytes, StandardCharsets.UTF_8);
})
.flatMap(eventString -> {
log.debug(eventString);
if (eventString.startsWith("data: ")) {
String jsonPart = eventString.substring("data: ".length()).trim();
if (JSONUtil.isTypeJSON(jsonPart) && !jsonPart.equals("data: [DONE]")) {
JSONObject retJson;
try {
retJson = JSONUtil.parseObj(jsonPart);
} catch (Exception e) {
log.warn("解析失败");
return Mono.just(eventString);
}
// 修改content字段
JSONArray choices = retJson.getJSONArray("choices");
for (int i = 0; i < choices.size(); i++) {
JSONObject choice = choices.getJSONObject(i);
if (choice == null) {
break;
}
JSONObject delta = choice.getJSONObject("delta");
if (delta == null) {
break;
}
String reasoningContent = delta.getStr("reasoning_content");
String content = delta.getStr("content");
if (StrUtil.isNotBlank(reasoningContent)) {
if (!thinking[0]) {
thinking[0] = true;
delta.set("content", "<think>" + reasoningContent);
} else {
delta.set("content", reasoningContent);
}
} else {
if (thinking[0]) {
thinking[0] = false;
delta.set("content", "</think>" + (content == null ? "" : content));
}
}
}
// 重新生成事件字符串
String modifiedJson = retJson.toString();
return Mono.just("data: " + modifiedJson + "\n\n");
}
return Mono.just(eventString);
}
return Mono.just(eventString);
})
.map(str -> {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
return new DefaultDataBufferFactory().wrap(bytes);
});
// 创建新的ClientResponse,移除CONTENT_LENGTH头
ClientResponse modifiedResponse = ClientResponse.from(clientResponse)
.headers(headers -> headers.remove(HttpHeaders.CONTENT_LENGTH))
.body(modifiedBody)
.build();
return Mono.just(modifiedResponse);
});
// 将拦截器应用到 WebClient.Builder
webClientBuilder.filter(requestFilter).filter(responseFilter);
};
}
}
Comment From: KonngExplorer
@Ltyro I have an alternative solution: We can set up a WebClient interceptor to redirect the
reasoning_content
field from the DeepSeek official API into thecontent
field, wrapping it with<think></think>
tags. Afterwards, we can parse the chain-of-thought content from thecontent
field. This temporarily resolves the issue of the current Spring-AI version not supporting the retrieval of thereasoning_content
field.```java @Slf4j @Configuration public class WebClientConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean WebClientCustomizer webClientCustomizer() { return webClientBuilder -> { ExchangeFilterFunction requestFilter = ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { log.info("Request Method: {}", clientRequest.method()); log.info("Request URL: {}", clientRequest.url()); return Mono.just(clientRequest); });
// 响应拦截器 ExchangeFilterFunction responseFilter = ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { log.info("Intercepted Response Status: {}", clientResponse.statusCode()); final boolean[] thinking = {false}; // 处理SSE事件流 Flux<DataBuffer> modifiedBody = clientResponse.bodyToFlux(DataBuffer.class) .map(buf -> { byte[] bytes = new byte[buf.readableByteCount()]; buf.read(bytes); DataBufferUtils.release(buf); return new String(bytes, StandardCharsets.UTF_8); }) .flatMap(eventString -> { log.debug(eventString); if (eventString.startsWith("data: ")) { String jsonPart = eventString.substring("data: ".length()).trim(); if (JSONUtil.isTypeJSON(jsonPart) && !jsonPart.equals("data: [DONE]")) { JSONObject retJson; try { retJson = JSONUtil.parseObj(jsonPart); } catch (Exception e) { log.warn("解析失败"); return Mono.just(eventString); } // 修改content字段 JSONArray choices = retJson.getJSONArray("choices"); for (int i = 0; i < choices.size(); i++) { JSONObject choice = choices.getJSONObject(i); if (choice == null) { break; } JSONObject delta = choice.getJSONObject("delta"); if (delta == null) { break; } String reasoningContent = delta.getStr("reasoning_content"); String content = delta.getStr("content"); if (StrUtil.isNotBlank(reasoningContent)) { if (!thinking[0]) { thinking[0] = true; delta.set("content", "<think>" + reasoningContent); } else { delta.set("content", reasoningContent); } } else { if (thinking[0]) { thinking[0] = false; delta.set("content", "</think>" + (content == null ? "" : content)); } } } // 重新生成事件字符串 String modifiedJson = retJson.toString(); return Mono.just("data: " + modifiedJson + "\n\n"); } return Mono.just(eventString); } return Mono.just(eventString); }) .map(str -> { byte[] bytes = str.getBytes(StandardCharsets.UTF_8); return new DefaultDataBufferFactory().wrap(bytes); }); // 创建新的ClientResponse,移除CONTENT_LENGTH头 ClientResponse modifiedResponse = ClientResponse.from(clientResponse) .headers(headers -> headers.remove(HttpHeaders.CONTENT_LENGTH)) .body(modifiedBody) .build(); return Mono.just(modifiedResponse); }); // 将拦截器应用到 WebClient.Builder webClientBuilder.filter(requestFilter).filter(responseFilter); }; }
} ```
If you use a non-streaming API, you can use the RestClient interceptor to do something similar.
Comment From: apappascs
That's a nice one! Thank you for sharing @KonngExplorer