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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user