fix(transformation): merge consecutive system and developer messages for non-OpenAI providers and Responses API - #39282
Conversation
Greptile SummaryThis PR merges consecutive system and translated developer messages for non-OpenAI providers and combines Responses API instructions with leading system/developer input.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported loss of later
|
| Filename | Overview |
|---|---|
| litellm/llms/base_llm/base_utils.py | Adds shared content-merging helpers and folds consecutive system/developer messages while preserving the later message’s metadata. |
| litellm/responses/litellm_completion_transformation/transformation.py | Merges Responses API instructions into a leading system or developer input while retaining the original message shape. |
| tests/test_litellm/llms/base_llm/test_base_utils.py | Covers content combinations, cache metadata precedence, billing metadata boundaries, and consecutive-message behavior. |
| tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py | Covers instruction merging with leading system/developer input and fallback behavior for empty or user-only input. |
Reviews (2): Last reviewed commit: "fix(base_utils): keep later cache_contro..." | Re-trigger Greptile
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 2 · PR risk: 0/10 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@greptileai please re-review, both findings are addressed in 40fab55 with regression tests and the PR description is updated |
40fab55 to
bc97b84
Compare
|
Hi maintainers 👋 — this is ready for review. Rebased onto the latest It fixes #26879: consecutive system/developer messages are now merged into one before reaching non-OpenAI providers, on both the Responses API bridge and the chat completions path. 15 regression tests added. Could someone with write access take a look and approve? Happy to address any feedback. Thanks! |
bc97b84 to
ded5896
Compare
7d09c86 to
2184ce3
Compare
|
Hi maintainers 👋 this is ready for review: rebased on the latest It fixes #26879 by merging consecutive system/developer messages before they reach non-OpenAI providers, on both the Responses API bridge and the chat completions path. 15 regression tests added. Note: the last commit also drops a duplicate parameter in the Bedrock KB hook test that #39474 introduced on staging, since it was failing the required lint job for every PR. Happy to split it out if you'd rather land that separately. Could someone with write access take a look? Thanks! |
…for non-OpenAI providers and Responses API
…ries when merging system messages
… pass The tuple accumulator passed to reduce copied the whole prefix on every step, making map_developer_role_to_system_role O(n^2) in the number of messages. Message count is not bounded by default, so a caller could tie up a proxy worker with thousands of small messages before any provider call. Use a list and merge into its last element in place instead.
2184ce3 to
eecea12
Compare
|
Hi maintainers 👋 — this PR is ready for review. All 33 required checks pass, it's rebased on the latest The bug (#26879): providers that accept only a single system prompt (Anthropic, Bedrock, Gemini, etc.) reject requests when LiteLLM sends more than one system message. This happens in two places:
The fix: This also covers the chat completions case, which #26884 and #26888 don't address. Note on OSV Scan: the red mark is the new gitpython advisories (fix is 3.1.59), which affect every PR on staging today and are being handled in #39553. It's not a required check and this PR doesn't touch the lockfile. Could someone with write access take a look? Happy to address any feedback. Thanks! |
…em message for non-OpenAI providers A developer message that arrives after the first user turn (Codex CLI's per-turn permissions item, a re-injected instruction after compaction, a mid-conversation policy update) was translated to a system message in place, and system-first chat templates such as Fireworks' qwen3.8 reject any system message that is not at the beginning with 400. The fold that merges consecutive system messages never saw it because a user turn sat in between. Move every developer message that follows the leading system block to the end of that block before the fold, so it merges into the single leading system message; client-authored mid-conversation system messages stay where the client put them.
|
Superseded by #39852, an internal copy with your commits intact, so CircleCI's provider suites can run on a litellm_ branch. Thanks for the fix! |
TLDR
Problem this solves:
instructionsplusdeveloperinput produced severalsystemmessagessystemmessages also reached backends unmergeddevelopermessage placed after the first user turn (Codex CLI's per-turn permissions item, a re-injected instruction after compaction, a mid-conversation policy update) was translated to asystemmessage in place, so it landed mid-conversationSystem message must be at the beginning.How it solves it:
systemmessages stay where the client put theminstructionsinto a leading system or developer itemTrade-off called out for reviewers: a hoisted developer instruction is applied from the start of the conversation rather than at the position the client placed it. For system-first templates the alternative is a hard 400, and for templates that accept later system messages the instruction content is unchanged, only its position moves. Providers that receive the developer role natively (OpenAI o-series and gpt-5 family) are untouched
User Flow
Before: a Codex CLI session pointed at the gateway with a Fireworks qwen3.8 deployment dies on its first turn, and a developer instruction placed after the first input item fails the same way from any Responses API client
base_url = https://litellm-domain/v1,wire_api = "responses", modelfireworks-qwen3p8) and start a session in a repoWhat is the capital of France? Answer in one word.and Codex sends POST https://litellm-domain/v1/responses withinstructions, a developer item carrying its permissions text, a user item carrying AGENTS.md, and a user item carrying the promptjinja template rendering failed. System message must be at the beginning., and no replyinputof a user item, then a developer itemAnswer with exactly one word., then the user questionAfter: the same session answers on its first turn, and a developer instruction placed after the first input item is honored
base_url = https://litellm-domain/v1,wire_api = "responses", modelfireworks-qwen3p8) and start a session in a repoWhat is the capital of France? Answer in one word.and Codex sends POST https://litellm-domain/v1/responses withinstructions, a developer item carrying its permissions text, a user item carrying AGENTS.md, and a user item carrying the promptParisand the session keeps goinginputof a user item, then a developer itemAnswer with exactly one word., then the user questionParisRelevant issues
Fixes #26879
Linear ticket
Resolves LIT-7019
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Shared setup, identical for both legs. Two proxies from two worktrees, each with
--num_workers 2, hitting the real Fireworks API (real $$$)Codex CLI 0.145.0
config.toml(onlybase_urldiffers between legs):additional_drop_params: ["client_metadata"]is unrelated to this PR: Codex sendsclient_metadataon every request and Fireworks rejects it (tracked in #28539 and #36268)Before (4990f06)
Codex CLI first turn
LITELLM_QA_KEY=sk-lit7019 codexin a trusted repo, typeWhat is the capital of France? Answer with exactly one word.and press Enter. Codex sendsinstructions, a developer item with its permissions text, a user item with AGENTS.md, and the prompt/v1/responses, developer item after the first input item (the customer's captured shape)
Run
Observed
/v1/responses, instructions plus a leading developer item (Codex first turn shape)
Run
Observed
/v1/chat/completions, developer message after the first user turn
Run
Observed
/v1/messages, system prompt with a multi-turn conversation (control, unaffected path)
Run
Observed
After (25186b1)
Codex CLI first turn
Paris/permissions, pickApprove for me, then typeAnd of Germany? Same rule, one word.Codex now sends the customer's exact shape, a developer item after the first user turn: input rolesdeveloper, user, user, assistant, developer, userObserved:
Berlin, and the proxy log shows the hoist doing the work/v1/responses, developer item after the first input item (the customer's captured shape)
Run
Observed
/v1/responses, instructions plus a leading developer item (Codex first turn shape)
Run
Observed
/v1/chat/completions, developer message after the first user turn
Run
Observed
/v1/messages, system prompt with a multi-turn conversation (control, unaffected path)
Run
Observed
Type
🐛 Bug Fix
Caveats (if any)
Low
cache_control, the later marker wins, matching breakpoint semanticssystemmessages still stay where they areclient_metadatastill needsadditional_drop_params(Custom_openaiException - AsyncCompletions.create() got an unexpected keyword argument 'client_metadata' #28539, fix(responses): stop the chat completions bridge from leaking unknown request fields #36268)Final Attestation