fix(responses): extract system content blocks into instructions#23698
fix(responses): extract system content blocks into instructions#23698jo-nike wants to merge 1 commit intoBerriAI:mainfrom
Conversation
When bridging /chat/completions to the Responses API, system messages
with list content blocks (e.g. [{"type": "text", "text": "..."}]) were
passed through as role=system input items. The ChatGPT Codex API rejects
these with "System messages are not allowed".
Extract text from content blocks and concatenate into the instructions
parameter, matching the existing behavior for plain string content.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Closed, duplicate of #21192, not merged in main yet. |
Greptile SummaryThis PR fixes a bug where system messages with list-format content blocks (e.g. Key changes:
Issues found:
Confidence Score: 3/5
|
| Filename | Overview |
|---|---|
| litellm/completion_extras/litellm_responses_transformation/transformation.py | Core fix: system messages with list content blocks are now extracted into instructions instead of being forwarded as role=system input items. Contains a logic bug where all-non-text list content produces an empty string instructions rather than None. |
| tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py | Three new mock-only unit tests covering string content, list text-block content, and mixed multi-message scenarios. Tests are well-scoped and require no network access; edge cases (empty list, non-text blocks only) are not covered. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[system message] --> B{content type?}
B -->|str| C[append to instructions]
B -->|list| D[iterate blocks]
B -->|other / None| E[silently ignored]
D --> F{block type?}
F -->|'text'| G[collect text]
F -->|image / other| H[silently discarded]
G --> I{text_parts empty?}
I -->|No| J[join & append to instructions]
I -->|Yes ⚠️| K[extracted = empty string\ninstructions set to '' instead of None]
C --> L[Final instructions string]
J --> L
Last reviewed commit: 0c290ee
| extracted = " ".join(text_parts) | ||
| if instructions: | ||
| instructions = f"{instructions} {extracted}" | ||
| else: | ||
| instructions = extracted |
There was a problem hiding this comment.
Empty extracted silently sets instructions to ""
When content is a list where no block is of type == "text" (e.g. a system message containing only image blocks), text_parts will be empty and extracted will be "". The subsequent assignment then overwrites instructions with an empty string rather than leaving it as None, which changes the downstream behaviour — callers that check if instructions: or if instructions is not None: will now get different results.
Additionally, if instructions already has content and this code path runs, the resulting value has a trailing space: f"{instructions} ".
Consider guarding the assignment so it only fires when there is actually extracted text:
| extracted = " ".join(text_parts) | |
| if instructions: | |
| instructions = f"{instructions} {extracted}" | |
| else: | |
| instructions = extracted | |
| extracted = " ".join(text_parts) | |
| if extracted: | |
| if instructions: | |
| instructions = f"{instructions} {extracted}" | |
| else: | |
| instructions = extracted |
| elif isinstance(content, list): | ||
| # Extract text from content blocks (e.g. [{"type": "text", "text": "..."}]) | ||
| text_parts = [] | ||
| for block in content: | ||
| if isinstance(block, dict) and block.get("type") == "text": | ||
| text_parts.append(block.get("text", "")) | ||
| elif isinstance(block, str): | ||
| text_parts.append(block) | ||
| extracted = " ".join(text_parts) | ||
| if instructions: | ||
| instructions = f"{instructions} {extracted}" | ||
| else: | ||
| instructions = extracted |
There was a problem hiding this comment.
Non-text content blocks in system messages are silently discarded
The previous else branch forwarded non-string content (including list content with non-text blocks like image_url) into input_items so they weren't lost. The new code for the isinstance(content, list) path only collects type == "text" blocks and silently discards all other block types (e.g. images). While the Responses API instructions field is text-only, the silent drop could be surprising. Adding a verbose_logger.warning when non-text blocks are encountered would help authors of clients like Claude Code diagnose unexpected behaviour without impacting production paths.
Summary
[{"type": "text", "text": "..."}]) are now extracted into theinstructionsparameter when bridging chat completions to the Responses APIrole: systeminput item, which the ChatGPT Codex API rejects with"System messages are not allowed"Test plan
test_convert_system_message_string_to_instructions— verifies existing string behaviortest_convert_system_message_content_blocks_to_instructions— verifies the fix for list content blockstest_convert_multiple_system_messages_mixed_formats— verifies concatenation of mixed string + list system messages