Last active
March 31, 2026 03:37
-
-
Save cowwoc/1cb95025e8a375166e6a34092951fdf6 to your computer and use it in GitHub Desktop.
docker-compose.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you want to map each model type differently, you can do something like this: