Improve the formatting of tool calls, show thinking, treat Reasoning and Thinking as the same thing (sorry Kant) (#7626)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jh-block
2026-03-17 14:36:28 +01:00
committed by GitHub
parent 88a89a83e3
commit a762fe1000
21 changed files with 262 additions and 329 deletions
+67 -24
View File
@@ -26,13 +26,31 @@ where
{
use serde::de::Error;
let mut raw: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
let raw: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
// Filter out old "conversationCompacted" messages from pre-14.0
raw.retain(|item| item.get("type").and_then(|v| v.as_str()) != Some("conversationCompacted"));
let mut migrated = Vec::with_capacity(raw.len());
for item in raw {
match item.get("type").and_then(|v| v.as_str()) {
// Filter out old "conversationCompacted" messages from pre-14.0
Some("conversationCompacted") => {}
// Migrate old "reasoning" content to "thinking". Invalid legacy reasoning
// blocks are dropped so they don't fail deserialization.
Some("reasoning") => {
if let Some(text) = item.get("text").and_then(|v| v.as_str()) {
migrated.push(serde_json::json!({
"type": "thinking",
"thinking": text,
"signature": ""
}));
}
}
_ => migrated.push(item),
}
}
let mut content: Vec<MessageContent> = serde_json::from_value(serde_json::Value::Array(raw))
.map_err(|e| Error::custom(format!("Failed to deserialize MessageContent: {}", e)))?;
let mut content: Vec<MessageContent> =
serde_json::from_value(serde_json::Value::Array(migrated))
.map_err(|e| Error::custom(format!("Failed to deserialize MessageContent: {}", e)))?;
for message_content in &mut content {
if let MessageContent::Text(text_content) = message_content {
@@ -176,11 +194,6 @@ pub struct SystemNotificationContent {
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ReasoningContent {
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
/// Content passed inside a message, which can be both simple content and tool content
#[serde(tag = "type", rename_all = "camelCase")]
@@ -195,7 +208,6 @@ pub enum MessageContent {
Thinking(ThinkingContent),
RedactedThinking(RedactedThinkingContent),
SystemNotification(SystemNotificationContent),
Reasoning(ReasoningContent),
}
impl fmt::Display for MessageContent {
@@ -237,7 +249,6 @@ impl fmt::Display for MessageContent {
MessageContent::SystemNotification(r) => {
write!(f, "[SystemNotification: {}]", r.msg)
}
MessageContent::Reasoning(r) => write!(f, "[Reasoning: {}]", r.text),
}
}
}
@@ -450,10 +461,6 @@ impl MessageContent {
})
}
pub fn reasoning<S: Into<String>>(text: S) -> Self {
MessageContent::Reasoning(ReasoningContent { text: text.into() })
}
pub fn as_system_notification(&self) -> Option<&SystemNotificationContent> {
if let MessageContent::SystemNotification(ref notification) = self {
Some(notification)
@@ -525,14 +532,6 @@ impl MessageContent {
_ => None,
}
}
/// Get the reasoning content if this is a ReasoningContent variant
pub fn as_reasoning(&self) -> Option<&ReasoningContent> {
match self {
MessageContent::Reasoning(reasoning) => Some(reasoning),
_ => None,
}
}
}
impl From<Content> for MessageContent {
@@ -1109,6 +1108,50 @@ mod tests {
}
}
#[test]
fn test_deserialization_migrates_reasoning_to_thinking() {
let json = serde_json::json!({
"role": "assistant",
"created": 1740171566,
"content": [
{ "type": "reasoning", "text": "step by step" },
{ "type": "text", "text": "final answer" }
],
"metadata": { "agentVisible": true, "userVisible": true }
});
let message: Message = serde_json::from_value(json).unwrap();
assert_eq!(message.content.len(), 2);
let MessageContent::Thinking(thinking) = &message.content[0] else {
panic!("Expected Thinking content");
};
assert_eq!(thinking.thinking, "step by step");
assert!(thinking.signature.is_empty());
}
#[test]
fn test_deserialization_drops_invalid_reasoning_blocks() {
let json = serde_json::json!({
"role": "assistant",
"created": 1740171566,
"content": [
{ "type": "reasoning" },
{ "type": "reasoning", "text": 42 },
{ "type": "text", "text": "still here" }
],
"metadata": { "agentVisible": true, "userVisible": true }
});
let message: Message = serde_json::from_value(json).unwrap();
assert_eq!(message.content.len(), 1);
let MessageContent::Text(text) = &message.content[0] else {
panic!("Expected Text content");
};
assert_eq!(text.text, "still here");
}
#[test]
fn test_from_prompt_message_text() {
let prompt_content = PromptMessageContent::Text {