Skip to content

fix(responses): extract system content blocks into instructions#23698

Closed
jo-nike wants to merge 1 commit intoBerriAI:mainfrom
jo-nike:fix/chatgpt-system-content-blocks
Closed

fix(responses): extract system content blocks into instructions#23698
jo-nike wants to merge 1 commit intoBerriAI:mainfrom
jo-nike:fix/chatgpt-system-content-blocks

Conversation

@jo-nike
Copy link

@jo-nike jo-nike commented Mar 15, 2026

Summary

  • System messages with list content blocks (e.g. [{"type": "text", "text": "..."}]) are now extracted into the instructions parameter when bridging chat completions to the Responses API
  • Previously, list content was passed through as a role: system input item, which the ChatGPT Codex API rejects with "System messages are not allowed"
  • Clients like Claude Code send system prompts as structured content blocks, triggering this error

Test plan

  • Added test_convert_system_message_string_to_instructions — verifies existing string behavior
  • Added test_convert_system_message_content_blocks_to_instructions — verifies the fix for list content blocks
  • Added test_convert_multiple_system_messages_mixed_formats — verifies concatenation of mixed string + list system messages
  • Verified end-to-end with Claude Code → LiteLLM → ChatGPT Codex (gpt-5.3-codex, gpt-5.4)

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.
@vercel
Copy link

vercel bot commented Mar 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 15, 2026 7:37pm

Request Review

@jo-nike
Copy link
Author

jo-nike commented Mar 15, 2026

Closed, duplicate of #21192, not merged in main yet.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR fixes a bug where system messages with list-format content blocks (e.g. [{"type": "text", "text": "..."}]) were forwarded as role: system input items to the Responses API instead of being extracted into the instructions parameter — causing the ChatGPT Codex API to reject requests with "System messages are not allowed". The fix is minimal and well-targeted, and the three new tests validate the primary scenarios correctly.

Key changes:

  • convert_chat_completion_messages_to_responses_api in transformation.py now handles isinstance(content, list) for system messages by joining all type == "text" block texts and appending them to instructions, mirroring existing string-content handling.
  • The original else branch that passed list-content system messages through to input_items is removed.

Issues found:

  • When a system message's list content contains no type == "text" blocks (e.g. only image blocks), extracted is "" and instructions is set to an empty string instead of remaining None. This can change downstream behaviour for callers that distinguish between None and "". Additionally, if prior instructions exist, a trailing space is appended (see line 175–179).
  • Non-text content blocks (images, etc.) inside system-message lists are silently discarded. While the Responses API instructions field is text-only, a verbose_logger.warning would help client authors diagnose the silent drop.
  • Tests do not cover the edge case of a list-only system message with no text blocks.

Confidence Score: 3/5

  • Safe to merge for the primary use-case, but contains a logic bug for edge-case list content with no text blocks that sets instructions to "" instead of None.
  • The fix correctly addresses the stated bug for text-only content blocks and all new tests pass. However, the empty-extracted edge case can change observable downstream behaviour (empty string vs. None for instructions) and no test covers it. The impact is limited to unusual system messages with only non-text blocks, but it is a real regression path.
  • litellm/completion_extras/litellm_responses_transformation/transformation.py lines 175–179 — the guard around empty extracted is missing.

Important Files Changed

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
Loading

Last reviewed commit: 0c290ee

Comment on lines +175 to +179
extracted = " ".join(text_parts)
if instructions:
instructions = f"{instructions} {extracted}"
else:
instructions = extracted
Copy link
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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

Comment on lines +167 to +179
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
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

@codspeed-hq
Copy link
Contributor

codspeed-hq bot commented Mar 15, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing jo-nike:fix/chatgpt-system-content-blocks (0c290ee) with main (548e7eb)

Open in CodSpeed

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