Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions src/providers/anthropic/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -411,11 +414,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 {
Expand Down Expand Up @@ -447,8 +457,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;
}
};
Expand Down Expand Up @@ -480,6 +494,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;
}
};
Expand Down Expand Up @@ -619,7 +638,16 @@ 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"))
}
}

/// Convert Responses API content items to Anthropic content blocks.
Expand Down Expand Up @@ -1772,6 +1800,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![
Expand Down
6 changes: 6 additions & 0 deletions src/providers/azure_openai/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Comment thread
greptile-apps[bot] marked this conversation as resolved.
// Pre-serialize request body before retry loop to avoid repeated serialization
let body = serde_json::to_vec(&payload).unwrap_or_default();

Expand Down
77 changes: 72 additions & 5 deletions src/providers/bedrock/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -432,11 +435,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 {
Expand Down Expand Up @@ -466,7 +476,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(&msg.content);
if !text.is_empty() {
system_parts.push(text);
}
continue;
}
};
Expand Down Expand Up @@ -497,6 +511,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(&msg.content);
if !text.is_empty() {
system_parts.push(text);
}
continue;
}
};
Expand Down Expand Up @@ -630,7 +649,16 @@ 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"))])
}
}

/// Convert Responses API content items to Bedrock content blocks.
Expand Down Expand Up @@ -1729,6 +1757,45 @@ 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::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_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.\n\nUse markdown.")
);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].role, "user");
}
}

#[cfg(test)]
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Expand Down
28 changes: 28 additions & 0 deletions src/providers/convert_utils.rs
Original file line number Diff line number Diff line change
@@ -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")
}
1 change: 1 addition & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading