From 8339ebcc4de03eddfe0fbd573abd756e98705053 Mon Sep 17 00:00:00 2001 From: jibanez-staticduo Date: Wed, 9 Sep 2026 22:46:40 +0200 Subject: [PATCH] fix(chatgpt): preserve OAuth identity during realtime signaling --- litellm/llms/chatgpt/common_utils.py | 8 +++++ litellm/llms/chatgpt/images.py | 9 ++--- litellm/llms/custom_httpx/llm_http_handler.py | 13 +++++-- .../llms/chatgpt/test_realtime.py | 36 ++++++++++++++++++- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/litellm/llms/chatgpt/common_utils.py b/litellm/llms/chatgpt/common_utils.py index 35e32e4172f..27fc28d2cd7 100644 --- a/litellm/llms/chatgpt/common_utils.py +++ b/litellm/llms/chatgpt/common_utils.py @@ -4,6 +4,8 @@ Constants and helpers for ChatGPT subscription OAuth. import os import platform +from collections.abc import Mapping +from types import MappingProxyType from typing import Any, Final from uuid import uuid4 @@ -105,6 +107,12 @@ You are producing plain text that will later be styled by the CLI. Follow these """ +def without_oauth_identity_headers(headers: Mapping[str, object]) -> Mapping[str, object]: + return MappingProxyType( + {key: value for key, value in headers.items() if key.lower() not in ("authorization", "chatgpt-account-id")} + ) + + class ChatGPTAuthError(BaseLLMException): def __init__( self, diff --git a/litellm/llms/chatgpt/images.py b/litellm/llms/chatgpt/images.py index 82845aa05f4..8366e5016aa 100644 --- a/litellm/llms/chatgpt/images.py +++ b/litellm/llms/chatgpt/images.py @@ -16,6 +16,7 @@ from litellm.types.llms.openai import AllMessageValues, FileTypes from litellm.types.router import GenericLiteLLMParams from .authenticator import Authenticator +from .common_utils import without_oauth_identity_headers from .responses.transformation import ChatGPTResponsesAPIConfig @@ -49,12 +50,6 @@ def encode_reference( } -def without_image_identity_headers(headers: Mapping[str, object]) -> Mapping[str, object]: - return MappingProxyType( - {key: value for key, value in headers.items() if key.lower() not in ("authorization", "chatgpt-account-id")} - ) - - def image_headers( headers: Mapping[str, object], model: str, params: Mapping[str, object] ) -> dict[str, object]: # mutable-ok: image handler requires dictionaries @@ -64,7 +59,7 @@ def image_headers( litellm_params=GenericLiteLLMParams.model_validate(params), ) return { # mutable-ok: image handler requires dictionaries - **without_image_identity_headers(headers), + **without_oauth_identity_headers(headers), **auth_headers, "accept": "application/json", } diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 1c6cdefe907..09f698891e5 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -6390,6 +6390,9 @@ class BaseLLMHTTPHandler: - sdp: the SDP offer (text) - session: JSON string with {"type": "realtime", "model": "...", ...} """ + from litellm.llms.chatgpt.common_utils import without_oauth_identity_headers + from litellm.llms.chatgpt.realtime import ChatGPTRealtimeHTTPConfig + if client is None or not isinstance(client, AsyncHTTPHandler): async_httpx_client = get_async_httpx_client( llm_provider=litellm.LlmProviders.OPENAI, @@ -6407,7 +6410,11 @@ class BaseLLMHTTPHandler: } if extra_headers: - headers.update(extra_headers) + headers.update( + without_oauth_identity_headers(extra_headers) + if isinstance(provider_config, ChatGPTRealtimeHTTPConfig) + else extra_headers + ) # Build multipart form data: sdp + session JSON session_data: Final = session_config or {} @@ -6641,9 +6648,9 @@ class BaseLLMHTTPHandler: @staticmethod def _image_extra_headers(custom_llm_provider: str, headers: Mapping[str, object]) -> Mapping[str, object]: if custom_llm_provider == "chatgpt": - from litellm.llms.chatgpt.images import without_image_identity_headers + from litellm.llms.chatgpt.common_utils import without_oauth_identity_headers - return without_image_identity_headers(headers) + return without_oauth_identity_headers(headers) return headers def image_edit_handler( diff --git a/tests/test_litellm/llms/chatgpt/test_realtime.py b/tests/test_litellm/llms/chatgpt/test_realtime.py index f66a3a92ac4..d41b64f3178 100644 --- a/tests/test_litellm/llms/chatgpt/test_realtime.py +++ b/tests/test_litellm/llms/chatgpt/test_realtime.py @@ -27,7 +27,12 @@ async def test_chatgpt_call_keeps_oauth_and_frameless_session(chatgpt_tokens, ap sdp_body=b"v=0\r\n", session={"model": "chatgpt/gpt-live-1-codex", "audio": {"output": {"voice": "sol"}}}, extra_query={"intent": "quicksilver", "architecture": "avas"}, - extra_headers={"openai-alpha": "quicksilver=v2"}, + extra_headers={ + "openai-alpha": "quicksilver=v2", + "x-gateway-route": "voice", + "aUtHoRiZaTiOn": "Bearer wrong", + "CHATGPT-ACCOUNT-ID": "wrong", + }, client=client, ) assert response.extensions["chatgpt_realtime"]["api_base"] == (api_base or "https://api.openai.com/v1") @@ -36,6 +41,9 @@ async def test_chatgpt_call_keeps_oauth_and_frameless_session(chatgpt_tokens, ap assert requests[0].url.path == "/backend-api/codex/realtime/calls" assert requests[0].url.params["architecture"] == "avas" assert requests[0].headers["authorization"] == "Bearer test-token-" + "default" + assert requests[0].headers["chatgpt-account-id"] == "test-account-default" + assert requests[0].headers["openai-alpha"] == "quicksilver=v2" + assert requests[0].headers["x-gateway-route"] == "voice" assert json.loads(requests[0].content) == { "sdp": "v=0\r\n", "session": {"model": "gpt-live-1-codex", "audio": {"output": {"voice": "sol"}}}, @@ -43,6 +51,32 @@ async def test_chatgpt_call_keeps_oauth_and_frameless_session(chatgpt_tokens, ap await client.client.aclose() +@pytest.mark.asyncio +async def test_openai_call_preserves_explicit_identity_headers(): + requests = [] + + def respond(request): + requests.append(request) + return httpx.Response(201, text="v=0\r\n") + + client = AsyncHTTPHandler() + client.client = httpx.AsyncClient(transport=httpx.MockTransport(respond)) + try: + response = await litellm.arealtime_calls( + model="openai/gpt-realtime-1.5", + openai_ephemeral_key="original-key", + sdp_body=b"v=0\r\n", + extra_headers={"Authorization": "Bearer explicit-key", "chatgpt-account-id": "custom-account"}, + client=client, + ) + assert response.status_code == 201 + assert requests[0].headers["authorization"] == "Bearer explicit-key" + assert requests[0].headers["chatgpt-account-id"] == "custom-account" + assert requests[0].headers["content-type"].startswith("multipart/form-data") + finally: + await client.client.aclose() + + @pytest.mark.parametrize("model,endpoint", [("gpt-realtime-1.5", "realtime"), ("gpt-live-1-codex", "live")]) def test_realtime_uses_platform_endpoint_with_oauth_headers(model, endpoint, chatgpt_tokens, local_model_cost_map): handler = ChatGPTRealtime(