use crate::provider::model::ApiProtocol;
use crate::types::content::{Content, Message};
pub trait ContextTranslationStrategy: Send + Sync {
fn translate_for_provider(&self, messages: &[Message], target: ApiProtocol) -> Vec<Message>;
}
pub struct DefaultContextTranslation;
impl ContextTranslationStrategy for DefaultContextTranslation {
fn translate_for_provider(&self, messages: &[Message], target: ApiProtocol) -> Vec<Message> {
messages
.iter()
.map(|msg| translate_message(msg, target))
.collect()
}
}
fn translate_message(msg: &Message, target: ApiProtocol) -> Message {
match msg {
Message::Assistant {
content,
stop_reason,
model,
provider,
usage,
timestamp,
error_message,
} => {
let translated_content = content
.iter()
.filter_map(|c| translate_content(c, target))
.collect();
Message::Assistant {
content: translated_content,
stop_reason: stop_reason.clone(),
model: model.clone(),
provider: provider.clone(),
usage: usage.clone(),
timestamp: *timestamp,
error_message: error_message.clone(),
}
}
other => other.clone(),
}
}
fn translate_content(content: &Content, target: ApiProtocol) -> Option<Content> {
match content {
Content::Thinking { thinking, .. } => match target {
ApiProtocol::AnthropicMessages => Some(content.clone()),
ApiProtocol::OpenAiCompletions
| ApiProtocol::OpenAiResponses
| ApiProtocol::AzureOpenAiResponses => Some(Content::Text {
text: format!("[Reasoning] {}", thinking),
}),
_ => {
tracing::warn!(
"Dropping Content::Thinking for provider {:?} (unsupported)",
target
);
None
}
},
_ => Some(content.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::content::StopReason;
use crate::types::usage::Usage;
fn make_assistant_with_thinking() -> Message {
Message::Assistant {
content: vec![
Content::Thinking {
thinking: "Let me think...".to_string(),
signature: None,
},
Content::Text {
text: "Here is my answer.".to_string(),
},
],
stop_reason: StopReason::Stop,
model: "test".to_string(),
provider: "test".to_string(),
usage: Usage::default(),
timestamp: 0,
error_message: None,
}
}
#[test]
fn test_anthropic_keeps_thinking() {
let strategy = DefaultContextTranslation;
let msgs = vec![make_assistant_with_thinking()];
let result = strategy.translate_for_provider(&msgs, ApiProtocol::AnthropicMessages);
assert_eq!(result.len(), 1);
if let Message::Assistant { content, .. } = &result[0] {
assert_eq!(content.len(), 2);
assert!(matches!(&content[0], Content::Thinking { .. }));
} else {
panic!("Expected assistant message");
}
}
#[test]
fn test_openai_converts_thinking_to_text() {
let strategy = DefaultContextTranslation;
let msgs = vec![make_assistant_with_thinking()];
let result = strategy.translate_for_provider(&msgs, ApiProtocol::OpenAiCompletions);
if let Message::Assistant { content, .. } = &result[0] {
assert_eq!(content.len(), 2);
match &content[0] {
Content::Text { text } => assert!(text.starts_with("[Reasoning]")),
other => panic!("Expected Text, got {:?}", other),
}
}
}
#[test]
fn test_google_drops_thinking() {
let strategy = DefaultContextTranslation;
let msgs = vec![make_assistant_with_thinking()];
let result = strategy.translate_for_provider(&msgs, ApiProtocol::GoogleGenerativeAi);
if let Message::Assistant { content, .. } = &result[0] {
assert_eq!(content.len(), 1); assert!(matches!(&content[0], Content::Text { .. }));
}
}
#[test]
fn test_user_messages_pass_through() {
let strategy = DefaultContextTranslation;
let msgs = vec![Message::user("Hello")];
let result = strategy.translate_for_provider(&msgs, ApiProtocol::OpenAiCompletions);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], Message::User { .. }));
}
}