Skip to content

Fix Responses API developer/system message merging - #26888

Closed
Genmin wants to merge 4 commits into
BerriAI:litellm_internal_stagingfrom
Genmin:fix/responses-merge-system-messages
Closed

Genmin wants to merge 4 commits into
BerriAI:litellm_internal_stagingfrom
Genmin:fix/responses-merge-system-messages

Conversation

@Genmin

@Genmin Genmin commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • merge developer-role messages into the leading system message for non-OpenAI providers
  • preserve existing behavior when no developer role is present
  • cover the Responses API bridge path where instructions and developer input previously produced multiple system messages

Fixes #26879

Tests

  • uv run --extra proxy pytest tests/llm_translation/test_base_llm_base_utils.py -q
  • uv run --extra proxy black --check litellm/llms/base_llm/base_utils.py tests/llm_translation/test_base_llm_base_utils.py
  • uv run --extra proxy ruff check litellm/llms/base_llm/base_utils.py tests/llm_translation/test_base_llm_base_utils.py

@CLAassistant

CLAassistant commented Apr 30, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Genmin seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors map_developer_role_to_system_role to merge all system and developer role messages into a single leading system message, addressing a Responses API bridge path where instructions and a developer input produced duplicate system messages.

  • P1 – Backwards-incompatible reordering: The new logic unconditionally extracts every system/developer message from its original position and collapses them into one block at the front. Any existing caller with interleaved system messages (e.g. [system, user, developer]) will see different output than before; there is no flag to opt out of the new behavior.

Confidence Score: 3/5

Not safe to merge as-is; the change silently breaks message ordering for any caller that had interleaved system messages alongside developer messages.

A P1 backwards-incompatible behavioral change in a shared utility pulls the score below the P1 ceiling; the fix is targeted but lacks a feature flag, and the missing test case (developer message after a user turn) leaves the regression risk unverified.

litellm/llms/base_llm/base_utils.py — the core merge logic and its impact on callers with interleaved system/developer messages.

Important Files Changed

Filename Overview
litellm/llms/base_llm/base_utils.py Refactors map_developer_role_to_system_role to merge all system/developer messages into a single leading system message; introduces a backwards-incompatible reordering for existing callers that have interleaved system messages.
tests/llm_translation/test_base_llm_base_utils.py New test file covering the revised merge logic; tests are mock-only and cover core scenarios, but does not test the case where developer messages appear after user messages (the primary backwards-compat risk).

Reviews (1): Last reviewed commit: "fix: merge developer role into leading s..." | Re-trigger Greptile

Comment thread litellm/llms/base_llm/base_utils.py Outdated
Comment on lines +224 to +242
for m in messages:
if m["role"] in {"developer", "system"}:
if system_message is None:
system_message = dict(m)
system_message["role"] = "system"
system_contents.append(m["content"])
else:
non_system_messages.append(m)

if m["role"] == "developer":
verbose_logger.debug(
"Translating developer role to system role for non-OpenAI providers."
) # ensure user knows what's happening with their input.
new_messages.append({"role": "system", "content": m["content"]})
else:
new_messages.append(m)
return new_messages

if system_message is None:
return non_system_messages

system_message["content"] = _merge_system_message_contents(system_contents)
return [cast(AllMessageValues, system_message), *non_system_messages]

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.

P1 Backwards-incompatible reordering of messages

The new implementation moves every system/developer message — regardless of its original position — to the front as a single merged block. This silently reorders conversations for any existing caller that had interleaved system messages. Consider a prior call with [system("A"), user("Q"), developer("B")]:

  • Before: [system("A"), user("Q"), system("B")] — developer converted in place, user message stays between two system messages
  • After: [system("A\n\nB"), user("Q")] — the developer message is no longer after the user turn

Similarly, multiple developer-only messages that previously resulted in multiple system messages at their original positions now collapse into one at the front. Per the backwards-compatibility rule, behavioral changes like this should be gated behind a feature flag rather than applied unconditionally to all callers.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +258 to +260
if merged_blocks:
merged_blocks.append({"type": "text", "text": "\n\n"})
merged_blocks.extend(content_blocks)

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 Standalone separator text block

Inserting {"type": "text", "text": "\n\n"} as a dedicated content block between merged segments is non-standard. Some providers validate or render each content block independently, and a block that contains only whitespace can result in empty rendered content or validation errors. A more portable approach is to append the separator to the trailing text of the preceding block (or to the leading text of the next block) instead of injecting it as its own element.

@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.21429% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/base_llm/base_utils.py 98.21% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Genmin

Genmin commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Updated this to address the review concern and the patch coverage gate. The merge now only collapses the leading system/developer block that Responses API creates; any developer messages after a user turn are converted to system in place, preserving the prior ordering behavior. I also moved the focused tests into the core-utils test shard so Codecov should receive coverage for the changed utility.

@Genmin

Genmin commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Added the one missing coverage case Codecov was still flagging: a non-leading system message is now covered alongside the later developer-role conversion path.

Local validation:

  • /tmp/litellm-uv-venv/bin/uv run --extra proxy pytest tests/test_litellm/litellm_core_utils/test_base_llm_base_utils.py -q -> 6 passed
  • /tmp/litellm-uv-venv/bin/uv run --extra proxy pytest tests/test_litellm/litellm_core_utils/test_base_llm_base_utils.py --cov=litellm.llms.base_llm.base_utils --cov-report=term-missing -q -> 6 passed; the changed utility lines are covered, including the preserved non-leading system branch.

Bojun-Vvibe added a commit to Bojun-Vvibe/oss-contributions that referenced this pull request Apr 30, 2026
…ct, terminal LaTeX

- BerriAI/litellm#26888: leading developer/system run merge for Responses API (merge-as-is)
- BerriAI/litellm#26887: drop search tools at append-point when mixed with function decls (merge-as-is)
- QwenLM/qwen-code#3439: terminal LaTeX renderer with width-calc hook (needs-discussion)
@mateo-berri

Copy link
Copy Markdown
Contributor

Closing in favor of #39282, which merges translated developer messages into one leading system message and also covers the Codex first turn.

@mateo-berri mateo-berri closed this Sep 5, 2026
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.

[Bug] Multiple system messages sent to non-OpenAI providers when using Responses API with developer role messages

3 participants