use serde_json::json;
use tiycore::transform::{normalize_tool_call_id, transform_messages, ToolCallIdMapper};
use tiycore::types::*;
fn make_model(provider: Provider, api: Api, id: &str) -> Model {
Model::builder()
.id(id)
.name(id)
.api(api)
.provider(provider)
.base_url("http://test")
.context_window(128000)
.max_tokens(16384)
.build()
.unwrap()
}
fn make_assistant_msg(
provider: Provider,
api: Api,
model_id: &str,
content: Vec<ContentBlock>,
stop_reason: StopReason,
) -> AssistantMessage {
AssistantMessage {
role: Role::Assistant,
content,
api,
provider,
model: model_id.to_string(),
usage: Usage::default(),
stop_reason,
error_message: None,
response_id: None,
timestamp: chrono::Utc::now().timestamp_millis(),
}
}
#[test]
fn test_transform_messages_skips_error_messages() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::OpenAI,
Api::OpenAICompletions,
"gpt-4o",
vec![ContentBlock::Text(TextContent::new("Error occurred"))],
StopReason::Error,
)),
Message::User(UserMessage::text("Try again")),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 2);
assert!(result[0].is_user());
assert!(result[1].is_user());
}
#[test]
fn test_transform_messages_skips_aborted_messages() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::OpenAI,
Api::OpenAICompletions,
"gpt-4o",
vec![ContentBlock::Text(TextContent::new("Partial..."))],
StopReason::Aborted,
)),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 1);
assert!(result[0].is_user());
}
#[test]
fn test_transform_messages_keeps_valid_messages() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::OpenAI,
Api::OpenAICompletions,
"gpt-4o",
vec![ContentBlock::Text(TextContent::new("Hi there!"))],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 2);
assert!(result[0].is_user());
assert!(result[1].is_assistant());
}
#[test]
fn test_transform_thinking_same_model_preserved() {
let target = make_model(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
);
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent {
thinking: "Let me think...".to_string(),
thinking_signature: Some("sig123".to_string()),
redacted: false,
}),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 2);
if let Message::Assistant(ref a) = result[1] {
assert!(a.content[0].is_thinking());
assert_eq!(
a.content[0].as_thinking().unwrap().thinking,
"Let me think..."
);
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_thinking_cross_model_converts_to_text() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent::new("Deep thought here")),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 2);
if let Message::Assistant(ref a) = result[1] {
assert!(a.content[0].is_text());
let text = a.content[0].as_text().unwrap().text.as_str();
assert_eq!(text, "Deep thought here");
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_empty_thinking_is_dropped() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent::new(" ")),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
if let Message::Assistant(ref a) = result[1] {
assert_eq!(a.content.len(), 1);
assert!(a.content[0].is_text());
assert_eq!(a.content[0].as_text().unwrap().text, "Answer");
}
}
#[test]
fn test_transform_redacted_thinking_cross_model_is_dropped() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent {
thinking: String::new(),
thinking_signature: Some("opaque".to_string()),
redacted: true,
}),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
if let Message::Assistant(ref a) = result[1] {
assert_eq!(a.content.len(), 1);
assert!(a.content[0].is_text());
assert_eq!(a.content[0].as_text().unwrap().text, "Answer");
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_tool_call_drops_thought_signature_cross_model() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Google,
Api::GoogleGenerativeAi,
"gemini-2.0-flash",
vec![ContentBlock::ToolCall(ToolCall {
id: "call_1".to_string(),
name: "search".to_string(),
arguments: json!({"q": "test"}),
thought_signature: Some("sig_1".to_string()),
})],
StopReason::ToolUse,
)),
];
let result = transform_messages(&messages, &target, None);
if let Message::Assistant(ref a) = result[1] {
let tool_call = a.content[0].as_tool_call().expect("expected tool call");
assert!(tool_call.thought_signature.is_none());
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_orphan_tool_calls_get_synthetic_results() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Get weather")),
Message::Assistant(make_assistant_msg(
Provider::OpenAI,
Api::OpenAICompletions,
"gpt-4o",
vec![ContentBlock::ToolCall(ToolCall::new(
"call_1",
"get_weather",
json!({"city": "Tokyo"}),
))],
StopReason::ToolUse,
)),
Message::User(UserMessage::text("Never mind")),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 4);
assert!(result[0].is_user());
assert!(result[1].is_assistant());
assert!(result[2].is_tool_result()); assert!(result[3].is_user());
if let Message::ToolResult(ref tr) = result[2] {
assert!(tr.is_error);
assert_eq!(tr.tool_call_id, "call_1");
}
}
#[test]
fn test_transform_matched_tool_calls_no_synthetic() {
let target = make_model(Provider::OpenAI, Api::OpenAICompletions, "gpt-4o");
let messages = vec![
Message::User(UserMessage::text("Get weather")),
Message::Assistant(make_assistant_msg(
Provider::OpenAI,
Api::OpenAICompletions,
"gpt-4o",
vec![ContentBlock::ToolCall(ToolCall::new(
"call_1",
"get_weather",
json!({"city": "Tokyo"}),
))],
StopReason::ToolUse,
)),
Message::ToolResult(ToolResultMessage::text(
"call_1",
"get_weather",
"Sunny 25C",
false,
)),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 3); }
#[test]
fn test_transform_with_tool_call_id_normalization() {
let target = make_model(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
);
fn normalize(id: &str) -> String {
id.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.take(64)
.collect()
}
let messages = vec![
Message::User(UserMessage::text("Hi")),
Message::Assistant(make_assistant_msg(
Provider::OpenAI,
Api::OpenAICompletions,
"gpt-4o",
vec![ContentBlock::ToolCall(ToolCall::new(
"call+special/chars",
"tool",
json!({}),
))],
StopReason::ToolUse,
)),
Message::ToolResult(ToolResultMessage::text(
"call+special/chars",
"tool",
"result",
false,
)),
];
let result = transform_messages(&messages, &target, Some(&normalize));
if let Message::Assistant(ref a) = result[1] {
let tc = a.content[0].as_tool_call().unwrap();
assert!(!tc.id.contains('+'));
assert!(!tc.id.contains('/'));
}
if let Message::ToolResult(ref tr) = result[2] {
assert!(!tr.tool_call_id.contains('+'));
assert!(!tr.tool_call_id.contains('/'));
}
}
#[test]
fn test_normalize_for_anthropic_basic() {
let id = "call_abc123";
let normalized = normalize_tool_call_id(id, &Provider::Anthropic);
assert_eq!(normalized, "call_abc123");
}
#[test]
fn test_normalize_for_anthropic_special_chars() {
let id = "call_abc+def/ghi=jkl";
let normalized = normalize_tool_call_id(id, &Provider::Anthropic);
assert!(normalized
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'));
assert_eq!(normalized, "call_abc_def_ghi_jkl");
}
#[test]
fn test_normalize_for_anthropic_max_length() {
let id = "a".repeat(100);
let normalized = normalize_tool_call_id(&id, &Provider::Anthropic);
assert!(normalized.len() <= 64);
}
#[test]
fn test_normalize_for_anthropic_pipe_separated() {
let id = "call_123|very_long_reasoning_item_suffix";
let normalized = normalize_tool_call_id(id, &Provider::Anthropic);
assert!(!normalized.contains('|'));
assert_eq!(normalized, "call_123");
}
#[test]
fn test_normalize_for_openai_truncation() {
let id = "a".repeat(50);
let normalized = normalize_tool_call_id(&id, &Provider::OpenAI);
assert_eq!(normalized.len(), 40);
}
#[test]
fn test_normalize_for_openai_short_id() {
let id = "call_abc";
let normalized = normalize_tool_call_id(id, &Provider::OpenAI);
assert_eq!(normalized, "call_abc");
}
#[test]
fn test_normalize_for_groq_same_as_openai() {
let id = "a".repeat(50);
let normalized = normalize_tool_call_id(&id, &Provider::Groq);
assert_eq!(normalized.len(), 40);
}
#[test]
fn test_normalize_for_other_providers_passthrough() {
let id = "call+special/chars|and|pipes";
let normalized = normalize_tool_call_id(id, &Provider::Google);
assert_eq!(normalized, id); }
#[test]
fn test_mapper_basic() {
let mut mapper = ToolCallIdMapper::new(Provider::Anthropic);
let normalized = mapper.normalize("call_abc123");
assert_eq!(normalized, "call_abc123");
}
#[test]
fn test_mapper_caching() {
let mut mapper = ToolCallIdMapper::new(Provider::Anthropic);
let first = mapper.normalize("call_abc123");
let second = mapper.normalize("call_abc123");
assert_eq!(first, second);
}
#[test]
fn test_mapper_collision_handling() {
let mut mapper = ToolCallIdMapper::new(Provider::Anthropic);
let id1 = "call+abc";
let id2 = "call/abc";
let n1 = mapper.normalize(id1);
let n2 = mapper.normalize(id2);
assert_ne!(n1, n2);
assert_eq!(n1, "call_abc");
assert!(n2.starts_with("call_abc_"));
}
#[test]
fn test_mapper_denormalize() {
let mut mapper = ToolCallIdMapper::new(Provider::Anthropic);
mapper.normalize("call+abc");
let original = mapper.denormalize("call_abc");
assert_eq!(original, Some(&"call+abc".to_string()));
}
#[test]
fn test_mapper_denormalize_missing() {
let mapper = ToolCallIdMapper::new(Provider::Anthropic);
assert!(mapper.denormalize("nonexistent").is_none());
}
#[test]
fn test_transform_thinking_same_provider_different_model_preserves_structure() {
let target = make_model(Provider::Anthropic, Api::AnthropicMessages, "claude-opus-5");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent {
thinking: "Deep thought here".to_string(),
thinking_signature: Some("sig_abc".to_string()),
redacted: false,
}),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
assert_eq!(result.len(), 2);
if let Message::Assistant(ref a) = result[1] {
assert!(a.content[0].is_thinking(), "expected thinking block");
let thinking = a.content[0].as_thinking().unwrap();
assert_eq!(thinking.thinking, "Deep thought here");
assert!(
thinking.thinking_signature.is_none(),
"signature should be cleared for cross-model"
);
assert!(a.content[1].is_text());
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_thinking_same_provider_redacted_still_dropped() {
let target = make_model(Provider::Anthropic, Api::AnthropicMessages, "claude-opus-5");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent {
thinking: String::new(),
thinking_signature: Some("opaque".to_string()),
redacted: true,
}),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
if let Message::Assistant(ref a) = result[1] {
assert_eq!(a.content.len(), 1);
assert!(a.content[0].is_text());
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_thinking_same_provider_empty_thinking_dropped() {
let target = make_model(Provider::Anthropic, Api::AnthropicMessages, "claude-opus-5");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Anthropic,
Api::AnthropicMessages,
"claude-sonnet-4",
vec![
ContentBlock::Thinking(ThinkingContent::new(" ")),
ContentBlock::Text(TextContent::new("Answer")),
],
StopReason::Stop,
)),
];
let result = transform_messages(&messages, &target, None);
if let Message::Assistant(ref a) = result[1] {
assert_eq!(a.content.len(), 1);
assert!(a.content[0].is_text());
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_transform_thinking_same_provider_tool_call_signature_cleared() {
let target = make_model(Provider::Google, Api::GoogleGenerativeAi, "gemini-3.0-pro");
let messages = vec![
Message::User(UserMessage::text("Hello")),
Message::Assistant(make_assistant_msg(
Provider::Google,
Api::GoogleGenerativeAi,
"gemini-2.5-flash",
vec![ContentBlock::ToolCall(ToolCall {
id: "call_1".to_string(),
name: "search".to_string(),
arguments: json!({"q": "test"}),
thought_signature: Some("sig_1".to_string()),
})],
StopReason::ToolUse,
)),
];
let result = transform_messages(&messages, &target, None);
if let Message::Assistant(ref a) = result[1] {
let tool_call = a.content[0].as_tool_call().expect("expected tool call");
assert!(
tool_call.thought_signature.is_none(),
"thought_signature should be cleared for cross-model same-provider"
);
} else {
panic!("Expected assistant message");
}
}