From 2e1416276ef70a4799172878a8752f688c62eb42 Mon Sep 17 00:00:00 2001
From: ScriptSmith <ScriptSmith@users.noreply.github.com>
Date: Thu, 4 Jun 2026 23:02:23 +1000
Subject: [PATCH 1/2] Fold input system/developer messages into native provider
 system param

---
 src/providers/anthropic/convert.rs | 91 ++++++++++++++++++++++++++++--
 src/providers/azure_openai/mod.rs  |  6 ++
 src/providers/bedrock/convert.rs   | 87 ++++++++++++++++++++++++++--
 src/providers/vertex/convert.rs    | 91 +++++++++++++++++++++++++++---
 4 files changed, 259 insertions(+), 16 deletions(-)

diff --git a/src/providers/anthropic/convert.rs b/src/providers/anthropic/convert.rs
index 5a2952e5..6d4f4c0e 100644
--- a/src/providers/anthropic/convert.rs
+++ b/src/providers/anthropic/convert.rs
@@ -411,11 +411,18 @@ pub fn convert_responses_input_to_messages(
     input: Option<ResponsesInput>,
     instructions: Option<String>,
 ) -> (Option<String>, Vec<AnthropicMessage>) {
-    let system = instructions;
+    // Seed the system prompt with the top-level `instructions`, then fold in any
+    // system/developer messages found in the input. Anthropic has no native
+    // system role, so input system/developer messages must be merged here or
+    // they would be silently dropped.
+    let mut system_parts: Vec<String> = Vec::new();
+    if let Some(instructions) = instructions {
+        system_parts.push(instructions);
+    }
     let mut messages: Vec<AnthropicMessage> = Vec::new();
 
     let Some(input) = input else {
-        return (system, messages);
+        return (join_system_parts(system_parts), messages);
     };
 
     match input {
@@ -447,8 +454,12 @@ pub fn convert_responses_input_to_messages(
                             EasyInputMessageRole::User => "user",
                             EasyInputMessageRole::Assistant => "assistant",
                             EasyInputMessageRole::System | EasyInputMessageRole::Developer => {
-                                // System/developer messages are typically handled via instructions
-                                // but if they appear in input, we skip them (already in system)
+                                // Anthropic has no system role: fold system/developer
+                                // input messages into the system prompt.
+                                let text = easy_content_text(&msg.content);
+                                if !text.is_empty() {
+                                    system_parts.push(text);
+                                }
                                 continue;
                             }
                         };
@@ -480,6 +491,11 @@ pub fn convert_responses_input_to_messages(
                         let role = match msg.role {
                             InputMessageItemRole::User => "user",
                             InputMessageItemRole::System | InputMessageItemRole::Developer => {
+                                // Fold system/developer input messages into the system prompt.
+                                let text = input_content_text(&msg.content);
+                                if !text.is_empty() {
+                                    system_parts.push(text);
+                                }
                                 continue;
                             }
                         };
@@ -619,7 +635,36 @@ pub fn convert_responses_input_to_messages(
         }
     }
 
-    (system, messages)
+    (join_system_parts(system_parts), messages)
+}
+
+/// Join collected system/developer prompt parts with blank lines, or `None`.
+fn join_system_parts(parts: Vec<String>) -> Option<String> {
+    if parts.is_empty() {
+        None
+    } else {
+        Some(parts.join("\n\n"))
+    }
+}
+
+/// Extract the concatenated text from an easy-input message content value.
+fn easy_content_text(content: &EasyInputMessageContent) -> String {
+    match content {
+        EasyInputMessageContent::Text(text) => text.clone(),
+        EasyInputMessageContent::Parts(parts) => input_content_text(parts),
+    }
+}
+
+/// Extract the concatenated `input_text` from a list of input content items.
+fn input_content_text(parts: &[ResponseInputContentItem]) -> String {
+    parts
+        .iter()
+        .filter_map(|part| match part {
+            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
+            _ => None,
+        })
+        .collect::<Vec<_>>()
+        .join("\n\n")
 }
 
 /// Convert Responses API content items to Anthropic content blocks.
@@ -1772,6 +1817,42 @@ mod tests {
         assert_eq!(messages.len(), 1);
     }
 
+    #[test]
+    fn test_convert_responses_input_folds_system_messages() {
+        // System/developer messages in the input must be folded into the system
+        // prompt (Anthropic has no system role), not silently dropped.
+        let items = vec![
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: None,
+                role: EasyInputMessageRole::System,
+                content: EasyInputMessageContent::Text("Be concise.".to_string()),
+            }),
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: None,
+                role: EasyInputMessageRole::Developer,
+                content: EasyInputMessageContent::Text("Use markdown.".to_string()),
+            }),
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: None,
+                role: EasyInputMessageRole::User,
+                content: EasyInputMessageContent::Text("Hi".to_string()),
+            }),
+        ];
+
+        let (system, messages) = convert_responses_input_to_messages(
+            Some(ResponsesInput::Items(items)),
+            Some("You are a helpful assistant.".to_string()),
+        );
+
+        assert_eq!(
+            system,
+            Some("You are a helpful assistant.\n\nBe concise.\n\nUse markdown.".to_string())
+        );
+        // Only the user message survives as a turn.
+        assert_eq!(messages.len(), 1);
+        assert_eq!(messages[0].role, "user");
+    }
+
     #[test]
     fn test_convert_responses_input_easy_messages() {
         let items = vec![
diff --git a/src/providers/azure_openai/mod.rs b/src/providers/azure_openai/mod.rs
index 1365fbd7..05bdaba2 100644
--- a/src/providers/azure_openai/mod.rs
+++ b/src/providers/azure_openai/mod.rs
@@ -228,6 +228,12 @@ impl Provider for AzureOpenAIProvider {
         let timeout = self.timeout;
         let stream = payload.stream;
 
+        // Drop gateway-managed fields (store, background, models, provider,
+        // plugins, sovereignty_requirements, skills) that Hadrian consumes
+        // itself and the upstream must never see, mirroring the OpenAI adapter.
+        let mut payload = payload;
+        payload.strip_gateway_fields();
+
         // Pre-serialize request body before retry loop to avoid repeated serialization
         let body = serde_json::to_vec(&payload).unwrap_or_default();
 
diff --git a/src/providers/bedrock/convert.rs b/src/providers/bedrock/convert.rs
index a14ddef8..6c3c5288 100644
--- a/src/providers/bedrock/convert.rs
+++ b/src/providers/bedrock/convert.rs
@@ -432,11 +432,18 @@ pub(super) fn convert_responses_input_to_bedrock_messages(
     input: Option<ResponsesInput>,
     instructions: Option<String>,
 ) -> (Option<Vec<BedrockSystemContent>>, Vec<BedrockMessage>) {
-    let system = instructions.map(|text| vec![BedrockSystemContent::text(text)]);
+    // Seed the system blocks with the top-level `instructions`, then fold in any
+    // system/developer messages found in the input. Bedrock Converse has no
+    // system role inside the message list, so input system/developer messages
+    // must be merged here or they would be silently dropped.
+    let mut system_parts: Vec<String> = Vec::new();
+    if let Some(instructions) = instructions {
+        system_parts.push(instructions);
+    }
     let mut messages: Vec<BedrockMessage> = Vec::new();
 
     let Some(input) = input else {
-        return (system, messages);
+        return (join_system_parts_bedrock(system_parts), messages);
     };
 
     match input {
@@ -466,7 +473,11 @@ pub(super) fn convert_responses_input_to_bedrock_messages(
                             EasyInputMessageRole::User => "user",
                             EasyInputMessageRole::Assistant => "assistant",
                             EasyInputMessageRole::System | EasyInputMessageRole::Developer => {
-                                // System/developer messages are handled via instructions
+                                // Fold system/developer input messages into the system blocks.
+                                let text = easy_content_text_bedrock(&msg.content);
+                                if !text.is_empty() {
+                                    system_parts.push(text);
+                                }
                                 continue;
                             }
                         };
@@ -497,6 +508,11 @@ pub(super) fn convert_responses_input_to_bedrock_messages(
                         let role = match msg.role {
                             InputMessageItemRole::User => "user",
                             InputMessageItemRole::System | InputMessageItemRole::Developer => {
+                                // Fold system/developer input messages into the system blocks.
+                                let text = input_content_text_bedrock(&msg.content);
+                                if !text.is_empty() {
+                                    system_parts.push(text);
+                                }
                                 continue;
                             }
                         };
@@ -630,7 +646,36 @@ pub(super) fn convert_responses_input_to_bedrock_messages(
         }
     }
 
-    (system, messages)
+    (join_system_parts_bedrock(system_parts), messages)
+}
+
+/// Join collected system/developer prompt parts into Bedrock system blocks, or `None`.
+fn join_system_parts_bedrock(parts: Vec<String>) -> Option<Vec<BedrockSystemContent>> {
+    if parts.is_empty() {
+        None
+    } else {
+        Some(vec![BedrockSystemContent::text(parts.join("\n\n"))])
+    }
+}
+
+/// Extract the concatenated text from an easy-input message content value.
+fn easy_content_text_bedrock(content: &EasyInputMessageContent) -> String {
+    match content {
+        EasyInputMessageContent::Text(text) => text.clone(),
+        EasyInputMessageContent::Parts(parts) => input_content_text_bedrock(parts),
+    }
+}
+
+/// Extract the concatenated `input_text` from a list of input content items.
+fn input_content_text_bedrock(parts: &[ResponseInputContentItem]) -> String {
+    parts
+        .iter()
+        .filter_map(|part| match part {
+            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
+            _ => None,
+        })
+        .collect::<Vec<_>>()
+        .join("\n\n")
 }
 
 /// Convert Responses API content items to Bedrock content blocks.
@@ -1729,6 +1774,40 @@ mod tool_result_status_tests {
         let tool_result = json.get("toolResult").unwrap();
         assert_eq!(tool_result.get("status").unwrap(), "success");
     }
+
+    #[test]
+    fn test_responses_input_folds_system_messages() {
+        use crate::api_types::responses::EasyInputMessage;
+
+        // System/developer messages in the input must be folded into the system
+        // blocks (Bedrock Converse has no system role inside the message list).
+        let items = vec![
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: None,
+                role: EasyInputMessageRole::System,
+                content: EasyInputMessageContent::Text("Be concise.".to_string()),
+            }),
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: None,
+                role: EasyInputMessageRole::User,
+                content: EasyInputMessageContent::Text("Hi".to_string()),
+            }),
+        ];
+
+        let (system, messages) = convert_responses_input_to_bedrock_messages(
+            Some(ResponsesInput::Items(items)),
+            Some("You are a helpful assistant.".to_string()),
+        );
+
+        let system = system.expect("system blocks present");
+        assert_eq!(system.len(), 1);
+        assert_eq!(
+            system[0].text.as_deref(),
+            Some("You are a helpful assistant.\n\nBe concise.")
+        );
+        assert_eq!(messages.len(), 1);
+        assert_eq!(messages[0].role, "user");
+    }
 }
 
 #[cfg(test)]
diff --git a/src/providers/vertex/convert.rs b/src/providers/vertex/convert.rs
index dcf45ac9..37dfd8ff 100644
--- a/src/providers/vertex/convert.rs
+++ b/src/providers/vertex/convert.rs
@@ -500,15 +500,20 @@ pub(super) fn convert_responses_input_to_vertex(
     input: Option<ResponsesInput>,
     instructions: Option<String>,
 ) -> (Option<VertexContent>, Vec<VertexContent>) {
-    let system_instruction = instructions.map(|text| VertexContent {
-        role: "user".to_string(),
-        parts: vec![VertexPart::text(text)],
-    });
+    // Seed the system instruction with the top-level `instructions`, then fold
+    // in any system/developer messages found in the input. Gemini carries the
+    // system prompt in a dedicated `systemInstruction` field, so input
+    // system/developer messages must be merged here or they would be silently
+    // dropped.
+    let mut system_parts: Vec<String> = Vec::new();
+    if let Some(instructions) = instructions {
+        system_parts.push(instructions);
+    }
 
     let mut contents: Vec<VertexContent> = Vec::new();
 
     let Some(input) = input else {
-        return (system_instruction, contents);
+        return (join_system_parts_vertex(system_parts), contents);
     };
 
     // Track function call IDs to function names for tool results
@@ -541,7 +546,11 @@ pub(super) fn convert_responses_input_to_vertex(
                             EasyInputMessageRole::User => "user",
                             EasyInputMessageRole::Assistant => "model",
                             EasyInputMessageRole::System | EasyInputMessageRole::Developer => {
-                                // System/developer messages handled via instructions
+                                // Fold system/developer input messages into the system instruction.
+                                let text = easy_content_text_vertex(&msg.content);
+                                if !text.is_empty() {
+                                    system_parts.push(text);
+                                }
                                 continue;
                             }
                         };
@@ -577,6 +586,11 @@ pub(super) fn convert_responses_input_to_vertex(
                         let role = match msg.role {
                             InputMessageItemRole::User => "user",
                             InputMessageItemRole::System | InputMessageItemRole::Developer => {
+                                // Fold system/developer input messages into the system instruction.
+                                let text = input_content_text_vertex(&msg.content);
+                                if !text.is_empty() {
+                                    system_parts.push(text);
+                                }
                                 continue;
                             }
                         };
@@ -706,7 +720,39 @@ pub(super) fn convert_responses_input_to_vertex(
         }
     }
 
-    (system_instruction, contents)
+    (join_system_parts_vertex(system_parts), contents)
+}
+
+/// Join collected system/developer prompt parts into a Vertex system instruction, or `None`.
+fn join_system_parts_vertex(parts: Vec<String>) -> Option<VertexContent> {
+    if parts.is_empty() {
+        None
+    } else {
+        Some(VertexContent {
+            role: "user".to_string(),
+            parts: vec![VertexPart::text(parts.join("\n\n"))],
+        })
+    }
+}
+
+/// Extract the concatenated text from an easy-input message content value.
+fn easy_content_text_vertex(content: &EasyInputMessageContent) -> String {
+    match content {
+        EasyInputMessageContent::Text(text) => text.clone(),
+        EasyInputMessageContent::Parts(parts) => input_content_text_vertex(parts),
+    }
+}
+
+/// Extract the concatenated `input_text` from a list of input content items.
+fn input_content_text_vertex(parts: &[ResponseInputContentItem]) -> String {
+    parts
+        .iter()
+        .filter_map(|part| match part {
+            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
+            _ => None,
+        })
+        .collect::<Vec<_>>()
+        .join("\n\n")
 }
 
 /// Convert Responses API content items to Vertex parts.
@@ -1212,6 +1258,37 @@ mod responses_api_tests {
         assert_eq!(contents[0].role, "user");
     }
 
+    #[test]
+    fn test_convert_responses_input_folds_system_messages() {
+        // System/developer messages in the input must be folded into the system
+        // instruction, not silently dropped.
+        let input = Some(ResponsesInput::Items(vec![
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: Some(MessageType::Message),
+                role: EasyInputMessageRole::System,
+                content: EasyInputMessageContent::Text("Be concise.".to_string()),
+            }),
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: Some(MessageType::Message),
+                role: EasyInputMessageRole::User,
+                content: EasyInputMessageContent::Text("Hi".to_string()),
+            }),
+        ]));
+
+        let (system, contents) =
+            convert_responses_input_to_vertex(input, Some("You are helpful.".to_string()));
+
+        let system = system.expect("system instruction present");
+        assert_eq!(system.role, "user");
+        assert_eq!(system.parts.len(), 1);
+        assert_eq!(
+            system.parts[0].text,
+            Some("You are helpful.\n\nBe concise.".to_string())
+        );
+        assert_eq!(contents.len(), 1);
+        assert_eq!(contents[0].role, "user");
+    }
+
     #[test]
     fn test_convert_responses_input_easy_messages() {
         let input = Some(ResponsesInput::Items(vec![

From a444f7c464f0610f7ff05323927dd137f35534ef Mon Sep 17 00:00:00 2001
From: ScriptSmith <ScriptSmith@users.noreply.github.com>
Date: Sat, 6 Jun 2026 20:27:44 +1000
Subject: [PATCH 2/2] Review fixes

---
 src/providers/anthropic/convert.rs | 25 ++++-----------------
 src/providers/bedrock/convert.rs   | 36 ++++++++++--------------------
 src/providers/convert_utils.rs     | 28 +++++++++++++++++++++++
 src/providers/mod.rs               |  1 +
 src/providers/vertex/convert.rs    | 29 +++++-------------------
 5 files changed, 51 insertions(+), 68 deletions(-)
 create mode 100644 src/providers/convert_utils.rs

diff --git a/src/providers/anthropic/convert.rs b/src/providers/anthropic/convert.rs
index 6d4f4c0e..efcedd9c 100644
--- a/src/providers/anthropic/convert.rs
+++ b/src/providers/anthropic/convert.rs
@@ -26,7 +26,10 @@ use crate::{
             ResponsesUsageInputTokensDetails, ResponsesUsageOutputTokensDetails,
         },
     },
-    providers::image::parse_data_url,
+    providers::{
+        convert_utils::{easy_content_text, input_content_text},
+        image::parse_data_url,
+    },
     services::FileSearchToolArguments,
 };
 
@@ -647,26 +650,6 @@ fn join_system_parts(parts: Vec<String>) -> Option<String> {
     }
 }
 
-/// Extract the concatenated text from an easy-input message content value.
-fn easy_content_text(content: &EasyInputMessageContent) -> String {
-    match content {
-        EasyInputMessageContent::Text(text) => text.clone(),
-        EasyInputMessageContent::Parts(parts) => input_content_text(parts),
-    }
-}
-
-/// Extract the concatenated `input_text` from a list of input content items.
-fn input_content_text(parts: &[ResponseInputContentItem]) -> String {
-    parts
-        .iter()
-        .filter_map(|part| match part {
-            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
-            _ => None,
-        })
-        .collect::<Vec<_>>()
-        .join("\n\n")
-}
-
 /// Convert Responses API content items to Anthropic content blocks.
 pub fn convert_responses_content_to_blocks(
     items: &[ResponseInputContentItem],
diff --git a/src/providers/bedrock/convert.rs b/src/providers/bedrock/convert.rs
index 6c3c5288..bbff7be3 100644
--- a/src/providers/bedrock/convert.rs
+++ b/src/providers/bedrock/convert.rs
@@ -27,7 +27,10 @@ use crate::{
             ResponsesUsage, ResponsesUsageInputTokensDetails, ResponsesUsageOutputTokensDetails,
         },
     },
-    providers::image::parse_data_url,
+    providers::{
+        convert_utils::{easy_content_text, input_content_text},
+        image::parse_data_url,
+    },
     services::FileSearchToolArguments,
 };
 
@@ -474,7 +477,7 @@ pub(super) fn convert_responses_input_to_bedrock_messages(
                             EasyInputMessageRole::Assistant => "assistant",
                             EasyInputMessageRole::System | EasyInputMessageRole::Developer => {
                                 // Fold system/developer input messages into the system blocks.
-                                let text = easy_content_text_bedrock(&msg.content);
+                                let text = easy_content_text(&msg.content);
                                 if !text.is_empty() {
                                     system_parts.push(text);
                                 }
@@ -509,7 +512,7 @@ pub(super) fn convert_responses_input_to_bedrock_messages(
                             InputMessageItemRole::User => "user",
                             InputMessageItemRole::System | InputMessageItemRole::Developer => {
                                 // Fold system/developer input messages into the system blocks.
-                                let text = input_content_text_bedrock(&msg.content);
+                                let text = input_content_text(&msg.content);
                                 if !text.is_empty() {
                                     system_parts.push(text);
                                 }
@@ -658,26 +661,6 @@ fn join_system_parts_bedrock(parts: Vec<String>) -> Option<Vec<BedrockSystemCont
     }
 }
 
-/// Extract the concatenated text from an easy-input message content value.
-fn easy_content_text_bedrock(content: &EasyInputMessageContent) -> String {
-    match content {
-        EasyInputMessageContent::Text(text) => text.clone(),
-        EasyInputMessageContent::Parts(parts) => input_content_text_bedrock(parts),
-    }
-}
-
-/// Extract the concatenated `input_text` from a list of input content items.
-fn input_content_text_bedrock(parts: &[ResponseInputContentItem]) -> String {
-    parts
-        .iter()
-        .filter_map(|part| match part {
-            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
-            _ => None,
-        })
-        .collect::<Vec<_>>()
-        .join("\n\n")
-}
-
 /// Convert Responses API content items to Bedrock content blocks.
 ///
 /// When content items have `cache_control`, a separate `cachePoint` block is inserted
@@ -1787,6 +1770,11 @@ mod tool_result_status_tests {
                 role: EasyInputMessageRole::System,
                 content: EasyInputMessageContent::Text("Be concise.".to_string()),
             }),
+            ResponsesInputItem::EasyMessage(EasyInputMessage {
+                type_: None,
+                role: EasyInputMessageRole::Developer,
+                content: EasyInputMessageContent::Text("Use markdown.".to_string()),
+            }),
             ResponsesInputItem::EasyMessage(EasyInputMessage {
                 type_: None,
                 role: EasyInputMessageRole::User,
@@ -1803,7 +1791,7 @@ mod tool_result_status_tests {
         assert_eq!(system.len(), 1);
         assert_eq!(
             system[0].text.as_deref(),
-            Some("You are a helpful assistant.\n\nBe concise.")
+            Some("You are a helpful assistant.\n\nBe concise.\n\nUse markdown.")
         );
         assert_eq!(messages.len(), 1);
         assert_eq!(messages[0].role, "user");
diff --git a/src/providers/convert_utils.rs b/src/providers/convert_utils.rs
new file mode 100644
index 00000000..0d3f8d56
--- /dev/null
+++ b/src/providers/convert_utils.rs
@@ -0,0 +1,28 @@
+//! Small text-extraction helpers shared by the provider request converters.
+//!
+//! Anthropic, Bedrock, and Vertex all need to pull the plain text out of a
+//! Responses-API message so it can be folded into their native system prompt.
+//! The logic is provider-agnostic (it only touches shared `api_types`), so it
+//! lives here rather than being duplicated per provider.
+
+use crate::api_types::responses::{EasyInputMessageContent, ResponseInputContentItem};
+
+/// Extract the concatenated text from an easy-input message content value.
+pub(crate) fn easy_content_text(content: &EasyInputMessageContent) -> String {
+    match content {
+        EasyInputMessageContent::Text(text) => text.clone(),
+        EasyInputMessageContent::Parts(parts) => input_content_text(parts),
+    }
+}
+
+/// Extract the concatenated `input_text` from a list of input content items.
+pub(crate) fn input_content_text(parts: &[ResponseInputContentItem]) -> String {
+    parts
+        .iter()
+        .filter_map(|part| match part {
+            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
+            _ => None,
+        })
+        .collect::<Vec<_>>()
+        .join("\n\n")
+}
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index e46ef743..b050ad4e 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -44,6 +44,7 @@ pub mod azure_openai;
 #[cfg(feature = "provider-bedrock")]
 pub mod bedrock;
 pub mod circuit_breaker;
+pub(crate) mod convert_utils;
 pub mod error;
 pub mod fallback;
 pub mod health_check;
diff --git a/src/providers/vertex/convert.rs b/src/providers/vertex/convert.rs
index 37dfd8ff..7c6911a4 100644
--- a/src/providers/vertex/convert.rs
+++ b/src/providers/vertex/convert.rs
@@ -28,7 +28,10 @@ use crate::{
             ResponsesUsageOutputTokensDetails,
         },
     },
-    providers::image::parse_data_url,
+    providers::{
+        convert_utils::{easy_content_text, input_content_text},
+        image::parse_data_url,
+    },
     services::FileSearchToolArguments,
 };
 
@@ -547,7 +550,7 @@ pub(super) fn convert_responses_input_to_vertex(
                             EasyInputMessageRole::Assistant => "model",
                             EasyInputMessageRole::System | EasyInputMessageRole::Developer => {
                                 // Fold system/developer input messages into the system instruction.
-                                let text = easy_content_text_vertex(&msg.content);
+                                let text = easy_content_text(&msg.content);
                                 if !text.is_empty() {
                                     system_parts.push(text);
                                 }
@@ -587,7 +590,7 @@ pub(super) fn convert_responses_input_to_vertex(
                             InputMessageItemRole::User => "user",
                             InputMessageItemRole::System | InputMessageItemRole::Developer => {
                                 // Fold system/developer input messages into the system instruction.
-                                let text = input_content_text_vertex(&msg.content);
+                                let text = input_content_text(&msg.content);
                                 if !text.is_empty() {
                                     system_parts.push(text);
                                 }
@@ -735,26 +738,6 @@ fn join_system_parts_vertex(parts: Vec<String>) -> Option<VertexContent> {
     }
 }
 
-/// Extract the concatenated text from an easy-input message content value.
-fn easy_content_text_vertex(content: &EasyInputMessageContent) -> String {
-    match content {
-        EasyInputMessageContent::Text(text) => text.clone(),
-        EasyInputMessageContent::Parts(parts) => input_content_text_vertex(parts),
-    }
-}
-
-/// Extract the concatenated `input_text` from a list of input content items.
-fn input_content_text_vertex(parts: &[ResponseInputContentItem]) -> String {
-    parts
-        .iter()
-        .filter_map(|part| match part {
-            ResponseInputContentItem::InputText { text, .. } => Some(text.as_str()),
-            _ => None,
-        })
-        .collect::<Vec<_>>()
-        .join("\n\n")
-}
-
 /// Convert Responses API content items to Vertex parts.
 pub(super) fn convert_responses_content_to_vertex_parts(
     items: &[ResponseInputContentItem],
