phi_core/provider/
context_translation.rs1use crate::provider::model::ApiProtocol;
10use crate::types::content::{Content, Message};
11
12pub trait ContextTranslationStrategy: Send + Sync {
18 fn translate_for_provider(&self, messages: &[Message], target: ApiProtocol) -> Vec<Message>;
20}
21
22pub struct DefaultContextTranslation;
29
30impl ContextTranslationStrategy for DefaultContextTranslation {
31 fn translate_for_provider(&self, messages: &[Message], target: ApiProtocol) -> Vec<Message> {
32 messages
33 .iter()
34 .map(|msg| translate_message(msg, target))
35 .collect()
36 }
37}
38
39fn translate_message(msg: &Message, target: ApiProtocol) -> Message {
40 match msg {
41 Message::Assistant {
42 content,
43 stop_reason,
44 model,
45 provider,
46 usage,
47 timestamp,
48 error_message,
49 } => {
50 let translated_content = content
51 .iter()
52 .filter_map(|c| translate_content(c, target))
53 .collect();
54 Message::Assistant {
55 content: translated_content,
56 stop_reason: stop_reason.clone(),
57 model: model.clone(),
58 provider: provider.clone(),
59 usage: usage.clone(),
60 timestamp: *timestamp,
61 error_message: error_message.clone(),
62 }
63 }
64 other => other.clone(),
66 }
67}
68
69fn translate_content(content: &Content, target: ApiProtocol) -> Option<Content> {
70 match content {
71 Content::Thinking { thinking, .. } => match target {
72 ApiProtocol::AnthropicMessages => Some(content.clone()),
74 ApiProtocol::OpenAiCompletions
76 | ApiProtocol::OpenAiResponses
77 | ApiProtocol::AzureOpenAiResponses => Some(Content::Text {
78 text: format!("[Reasoning] {}", thinking),
79 }),
80 _ => {
82 tracing::warn!(
83 "Dropping Content::Thinking for provider {:?} (unsupported)",
84 target
85 );
86 None
87 }
88 },
89 _ => Some(content.clone()),
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::types::content::StopReason;
98 use crate::types::usage::Usage;
99
100 fn make_assistant_with_thinking() -> Message {
101 Message::Assistant {
102 content: vec![
103 Content::Thinking {
104 thinking: "Let me think...".to_string(),
105 signature: None,
106 },
107 Content::Text {
108 text: "Here is my answer.".to_string(),
109 },
110 ],
111 stop_reason: StopReason::Stop,
112 model: "test".to_string(),
113 provider: "test".to_string(),
114 usage: Usage::default(),
115 timestamp: 0,
116 error_message: None,
117 }
118 }
119
120 #[test]
121 fn test_anthropic_keeps_thinking() {
122 let strategy = DefaultContextTranslation;
123 let msgs = vec![make_assistant_with_thinking()];
124 let result = strategy.translate_for_provider(&msgs, ApiProtocol::AnthropicMessages);
125 assert_eq!(result.len(), 1);
126 if let Message::Assistant { content, .. } = &result[0] {
127 assert_eq!(content.len(), 2);
128 assert!(matches!(&content[0], Content::Thinking { .. }));
129 } else {
130 panic!("Expected assistant message");
131 }
132 }
133
134 #[test]
135 fn test_openai_converts_thinking_to_text() {
136 let strategy = DefaultContextTranslation;
137 let msgs = vec![make_assistant_with_thinking()];
138 let result = strategy.translate_for_provider(&msgs, ApiProtocol::OpenAiCompletions);
139 if let Message::Assistant { content, .. } = &result[0] {
140 assert_eq!(content.len(), 2);
141 match &content[0] {
142 Content::Text { text } => assert!(text.starts_with("[Reasoning]")),
143 other => panic!("Expected Text, got {:?}", other),
144 }
145 }
146 }
147
148 #[test]
149 fn test_google_drops_thinking() {
150 let strategy = DefaultContextTranslation;
151 let msgs = vec![make_assistant_with_thinking()];
152 let result = strategy.translate_for_provider(&msgs, ApiProtocol::GoogleGenerativeAi);
153 if let Message::Assistant { content, .. } = &result[0] {
154 assert_eq!(content.len(), 1); assert!(matches!(&content[0], Content::Text { .. }));
156 }
157 }
158
159 #[test]
160 fn test_user_messages_pass_through() {
161 let strategy = DefaultContextTranslation;
162 let msgs = vec![Message::user("Hello")];
163 let result = strategy.translate_for_provider(&msgs, ApiProtocol::OpenAiCompletions);
164 assert_eq!(result.len(), 1);
165 assert!(matches!(&result[0], Message::User { .. }));
166 }
167}