fix(chatgpt): preserve OAuth identity during realtime signaling

This commit is contained in:
jibanez-staticduo 2026-09-09 22:46:40 +02:00
parent 023f3e93e7
commit 8339ebcc4d
No known key found for this signature in database
4 changed files with 55 additions and 11 deletions

View file

@ -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,

View file

@ -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",
}

View file

@ -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(

View file

@ -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(