Skip to content

Fold input system/developer messages into native provider system param#48

Open
ScriptSmith wants to merge 1 commit into
mainfrom
fix/native-system-developer-messages
Open

Fold input system/developer messages into native provider system param#48
ScriptSmith wants to merge 1 commit into
mainfrom
fix/native-system-developer-messages

Conversation

@ScriptSmith
Copy link
Copy Markdown
Owner

No description provided.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 4, 2026

Greptile Summary

This PR fixes silent message loss across three LLM provider adapters (Anthropic, Bedrock, Vertex) and closes a field-leakage gap in the Azure OpenAI adapter. Previously, system and developer role messages appearing inside the input array were silently discarded because these providers have no mid-conversation system role; they are now correctly accumulated and merged into the provider's native system parameter before the request is forwarded.

  • Anthropic / Bedrock / Vertex: system_parts collects the top-level instructions string first, then appends text from any System/Developer messages in the input; a join_system_parts_* helper joins them with \n\n and wraps in the provider's expected type. Each provider ships a new test covering the folding path.
  • Azure OpenAI: strip_gateway_fields() is now called before serialization, matching the existing OpenAI adapter and preventing gateway-internal fields from leaking to the upstream endpoint.
  • Code duplication: the six text-extraction helpers (easy_content_text_* / input_content_text_*) are logically identical across all three provider modules and are good candidates for a shared utility module.

Confidence Score: 4/5

The change is safe to merge; the core folding logic is correct and well-tested across all three providers, and no existing behaviour is regressed.

The system-message folding is implemented consistently and symmetrically across Anthropic, Bedrock, and Vertex. Each provider's join_system_parts correctly returns None when there is nothing to merge, matching the prior behaviour when both instructions and the input are empty. The only issues are style-level: six text-extraction helpers are copy-pasted across three files, the Bedrock test omits the Developer role, and the Azure OpenAI field-stripping fix is an unrelated change bundled into the PR.

The duplicated helper functions in anthropic/convert.rs, bedrock/convert.rs, and vertex/convert.rs are the main thing worth revisiting before the pattern spreads further.

Important Files Changed

Filename Overview
src/providers/anthropic/convert.rs Folds system/developer input messages into the Anthropic system prompt instead of silently dropping them; adds two new private text-extraction helpers (duplicated from the other providers) and a focused test covering both System and Developer roles.
src/providers/bedrock/convert.rs Mirrors the Anthropic change for Bedrock Converse: system/developer messages are now folded into the system blocks rather than dropped; adds duplicated text-extraction helpers and a test that covers System role but not Developer role.
src/providers/vertex/convert.rs Same system-message folding applied to the Vertex/Gemini adapter; system/developer messages are merged into the systemInstruction field with correct role: "user" and a test validates the joined output.
src/providers/azure_openai/mod.rs Adds a strip_gateway_fields() call before serialization so gateway-managed fields are no longer forwarded to the upstream Azure endpoint, mirroring the existing OpenAI adapter; this fix is unrelated to the PR's stated focus on system-message folding.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["convert_responses_input_to_messages()"] --> B{{"instructions present?"}}
    B -- yes --> C["system_parts.push(instructions)"]
    B -- no --> D["system_parts = []"]
    C --> E["Iterate input messages"]
    D --> E
    E --> F{{"message role?"}}
    F -- "System / Developer" --> G["extract text"]
    G --> H{"text non-empty?"}
    H -- yes --> I["system_parts.push(text)"]
    H -- no --> E
    I --> E
    F -- "User / Assistant" --> J["append to messages[]"]
    J --> E
    E -- done --> K["join_system_parts(system_parts)"]
    K --> L{{"parts empty?"}}
    L -- yes --> M["return None"]
    L -- no --> N["return Some(parts.join('\n\n'))"]
    N --> O["Provider-specific wrapping"]
    O --> P["Anthropic: Option<String>"]
    O --> Q["Bedrock: Option<Vec<BedrockSystemContent>>"]
    O --> R["Vertex: Option<VertexContent>"]
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/providers/anthropic/convert.rs:650-668
**Duplicated text-extraction helpers across three provider files**

`easy_content_text` / `input_content_text` in `anthropic/convert.rs`, `easy_content_text_bedrock` / `input_content_text_bedrock` in `bedrock/convert.rs`, and `easy_content_text_vertex` / `input_content_text_vertex` in `vertex/convert.rs` are byte-for-byte identical in logic. The only difference is the suffix in the name. If the extraction logic ever needs to change (e.g., to handle a new `ResponseInputContentItem` variant), it must be updated in three places. Extracting them into a shared module (e.g., `src/providers/convert_utils.rs`) would eliminate the duplication.

### Issue 2 of 3
src/providers/bedrock/convert.rs:1774-1813
**Bedrock test omits the `Developer` role**

The new test exercises the `System` role being folded into the system blocks but doesn't also cover `EasyInputMessageRole::Developer`, even though both are handled by the same branch and the anthropic test does cover both. Adding a `Developer` message to the test item list would increase confidence that the symmetry holds under future refactors.

### Issue 3 of 3
src/providers/azure_openai/mod.rs:231-236
**Unrelated fix bundled into the system-message folding PR**

The `strip_gateway_fields()` call here mirrors the OpenAI adapter and is a distinct correctness fix — gateway-managed fields were previously leaking to the upstream Azure endpoint. Mixing it into a PR titled "fold input system/developer messages into native provider system param" makes the change harder to bisect. Consider whether it warrants its own PR or at least a clear note in the PR description.

Reviews (1): Last reviewed commit: "Fold input system/developer messages int..." | Re-trigger Greptile

Comment on lines +650 to 668
/// 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")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated text-extraction helpers across three provider files

easy_content_text / input_content_text in anthropic/convert.rs, easy_content_text_bedrock / input_content_text_bedrock in bedrock/convert.rs, and easy_content_text_vertex / input_content_text_vertex in vertex/convert.rs are byte-for-byte identical in logic. The only difference is the suffix in the name. If the extraction logic ever needs to change (e.g., to handle a new ResponseInputContentItem variant), it must be updated in three places. Extracting them into a shared module (e.g., src/providers/convert_utils.rs) would eliminate the duplication.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/anthropic/convert.rs
Line: 650-668

Comment:
**Duplicated text-extraction helpers across three provider files**

`easy_content_text` / `input_content_text` in `anthropic/convert.rs`, `easy_content_text_bedrock` / `input_content_text_bedrock` in `bedrock/convert.rs`, and `easy_content_text_vertex` / `input_content_text_vertex` in `vertex/convert.rs` are byte-for-byte identical in logic. The only difference is the suffix in the name. If the extraction logic ever needs to change (e.g., to handle a new `ResponseInputContentItem` variant), it must be updated in three places. Extracting them into a shared module (e.g., `src/providers/convert_utils.rs`) would eliminate the duplication.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 1774 to 1813
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)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Bedrock test omits the Developer role

The new test exercises the System role being folded into the system blocks but doesn't also cover EasyInputMessageRole::Developer, even though both are handled by the same branch and the anthropic test does cover both. Adding a Developer message to the test item list would increase confidence that the symmetry holds under future refactors.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/bedrock/convert.rs
Line: 1774-1813

Comment:
**Bedrock test omits the `Developer` role**

The new test exercises the `System` role being folded into the system blocks but doesn't also cover `EasyInputMessageRole::Developer`, even though both are handled by the same branch and the anthropic test does cover both. Adding a `Developer` message to the test item list would increase confidence that the symmetry holds under future refactors.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +231 to +236
// 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unrelated fix bundled into the system-message folding PR

The strip_gateway_fields() call here mirrors the OpenAI adapter and is a distinct correctness fix — gateway-managed fields were previously leaking to the upstream Azure endpoint. Mixing it into a PR titled "fold input system/developer messages into native provider system param" makes the change harder to bisect. Consider whether it warrants its own PR or at least a clear note in the PR description.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/azure_openai/mod.rs
Line: 231-236

Comment:
**Unrelated fix bundled into the system-message folding PR**

The `strip_gateway_fields()` call here mirrors the OpenAI adapter and is a distinct correctness fix — gateway-managed fields were previously leaking to the upstream Azure endpoint. Mixing it into a PR titled "fold input system/developer messages into native provider system param" makes the change harder to bisect. Consider whether it warrants its own PR or at least a clear note in the PR description.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant