Skip to content

Instantly share code, notes, and snippets.

@cowwoc
Last active March 31, 2026 03:37
Show Gist options
  • Select an option

  • Save cowwoc/1cb95025e8a375166e6a34092951fdf6 to your computer and use it in GitHub Desktop.

Select an option

Save cowwoc/1cb95025e8a375166e6a34092951fdf6 to your computer and use it in GitHub Desktop.
docker-compose.yml
"""
LiteLLM CustomLogger callback to strip system messages from requests
routed to ChatGPT subscription models.
System messages are merged into the first user message since the ChatGPT
Codex endpoint doesn't support the system role.
Registered via litellm_settings.callbacks in litellm-config.yaml.
"""
from typing import List, Dict, Any, Optional, Union
import logging
from litellm.integrations.custom_logger import CustomLogger
logger = logging.getLogger(__name__)
def _extract_text(content) -> str:
"""Extract plain text from message content (string or content-block list)."""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, dict):
parts.append(block.get("text", ""))
elif isinstance(block, str):
parts.append(block)
return "\n\n".join(p for p in parts if p)
return str(content) if content else ""
class ChatGPTSystemMessageCallback(CustomLogger):
"""
Transforms system messages into user messages before the request
is sent to the backend model.
"""
def __init__(self):
super().__init__()
self.name = "chatgpt-system-message-handler"
def transform_system_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Merge all system messages into the first user message, preserving
the relative order of non-system messages.
"""
if not messages:
return messages
system_parts: list[str] = []
non_system: list[Dict[str, Any]] = []
for msg in messages:
if msg.get("role") == "system":
text = _extract_text(msg.get("content", ""))
if text:
system_parts.append(text)
else:
non_system.append(msg)
if not system_parts:
return messages
merged_system = "\n\n".join(system_parts)
# Find first user message and prepend system content
for i, msg in enumerate(non_system):
if msg.get("role") == "user":
original = _extract_text(msg.get("content", ""))
non_system[i] = {
**msg,
"content": f"{merged_system}\n\n{original}".strip(),
}
break
else:
# No user message found — insert a synthetic one at the start
non_system.insert(0, {"role": "user", "content": merged_system})
logger.info("Merged %d system message(s) into first user message", len(system_parts))
return non_system
async def async_pre_call_hook(
self,
user_api_key_dict,
cache,
data: dict,
call_type: str,
) -> Optional[Union[Exception, str, dict]]:
"""
Called by LiteLLM before each LLM request.
Handles two system-message formats:
- OpenAI/chat format: system role entries in data["messages"]
- Anthropic format: top-level data["system"] field
"""
system_parts: list[str] = []
modified = False
# Anthropic format: top-level "system" field
system_field = data.get("system")
if system_field:
text = _extract_text(system_field)
if text:
system_parts.append(text)
data.pop("system", None)
modified = True
# OpenAI chat format: system-role entries in messages list
messages = data.get("messages") or []
non_system = []
for msg in messages:
if msg.get("role") == "system":
text = _extract_text(msg.get("content", ""))
if text:
system_parts.append(text)
modified = True
else:
non_system.append(msg)
if not modified or not system_parts:
return None
merged_system = "\n\n".join(system_parts)
# Prepend merged system text into the first user message
for i, msg in enumerate(non_system):
if msg.get("role") == "user":
original = _extract_text(msg.get("content", ""))
non_system[i] = {
**msg,
"content": f"{merged_system}\n\n{original}".strip(),
}
break
else:
non_system.insert(0, {"role": "user", "content": merged_system})
logger.info("Merged system prompt into first user message (model=%s call_type=%s)",
data.get("model"), call_type)
data["messages"] = non_system
return data
# Export an instance for LiteLLM's get_instance_fn
chatgpt_system_message_callback = ChatGPTSystemMessageCallback()
GNU nano 7.2 docker-compose.yml
name: litellm
services:
litellm:
image: ghcr.io/berriai/litellm:main-latest
ports:
- "4000:4000"
volumes:
- <host-absolute-path>/litellm-config.yaml:/app/config.yaml
- <host-absolute-path>/chatgpt_system_message_callback.py:/app/chatgpt_system_message_callback.py
- litellm-chatgpt-auth:/root/.config/litellm/chatgpt
environment:
- LITELLM_MASTER_KEY=sk-my-fake-litellm-key
- DATABASE_URL=postgresql://litellm:litellm@db:5432/litellm
- COOLDOWN_TTL=1800
# - LITELLM_LOG=DEBUG
command: ["--config", "/app/config.yaml", "--port", "4000"]
depends_on:
db:
condition: service_healthy
restart: on-failure
extra_hosts:
- "host.docker.internal:host-gateway"
db:
image: postgres:16
environment:
- POSTGRES_USER=litellm
- POSTGRES_PASSWORD=litellm
- POSTGRES_DB=litellm
volumes:
- litellm-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U litellm"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
litellm-db:
litellm-chatgpt-auth:
model_list:
- model_name: anthropic/*
litellm_params:
model: chatgpt/gpt-5.3-codex
mode: responses
litellm_settings:
callbacks: ["chatgpt_system_message_callback.chatgpt_system_message_callback"]
router_settings:
routing_strategy: usage-based-routing
general_settings:
master_key: sk-my-fake-litellm-key
database_url: postgresql://litellm:litellm@db:5432/litellm
@cowwoc

cowwoc commented Mar 31, 2026

Copy link
Copy Markdown
Author

chatgpt_system_message_callback.py is a workaround for BerriAI/litellm#22968

@cowwoc

cowwoc commented Mar 31, 2026

Copy link
Copy Markdown
Author

If you want to map each model type differently, you can do something like this:

model_list:
  - model_name: claude-opus-4-6
    litellm_params:
      model: chatgpt/gpt-5.4
      mode: responses
  - model_name: claude-sonnet-4-6
    litellm_params:
      model: chatgpt/gpt-5.3-codex
      mode: responses
  - model_name: claude-haiku-4-5-20251001
    litellm_params:
      model: chatgpt/gpt-5.2-codex
      mode: responses

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