mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-09 08:18:12 +00:00
Merge 2cddcf31b2 into f8f12a270a
This commit is contained in:
commit
fbd4bd8b61
3 changed files with 165 additions and 15 deletions
|
|
@ -9,13 +9,14 @@ import os
|
|||
import platform
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, TypeAlias
|
||||
from urllib.parse import urlsplit
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from pydantic import JsonValue, TypeAdapter
|
||||
from pydantic import JsonValue, TypeAdapter, ValidationError
|
||||
|
||||
from litellm.litellm_core_utils.get_provider_specific_headers import ProviderSpecificHeaderUtils
|
||||
from litellm.litellm_core_utils.provider_affinity import get_caller_litellm_session_id, get_requested_session_id
|
||||
|
|
@ -407,22 +408,58 @@ def _prompt_cache_session_id(prompt_cache_key: object) -> str | None:
|
|||
)
|
||||
|
||||
|
||||
def get_chatgpt_session_id(litellm_params: object) -> str | None:
|
||||
def get_explicit_chatgpt_session_id(litellm_params: object) -> str | None:
|
||||
params: Final = _normalize_litellm_params(litellm_params)
|
||||
return (
|
||||
get_caller_litellm_session_id(params)
|
||||
or get_requested_session_id(params)
|
||||
or _prompt_cache_session_id(params.get("prompt_cache_key"))
|
||||
or next(
|
||||
(
|
||||
str(value)
|
||||
for key in ("litellm_session_id", "litellm_trace_id", "litellm_call_id")
|
||||
if (value := params.get(key))
|
||||
),
|
||||
None,
|
||||
)
|
||||
or (str(params["litellm_session_id"]) if params.get("litellm_session_id") else None)
|
||||
)
|
||||
|
||||
|
||||
def get_chatgpt_session_id(litellm_params: object) -> str | None:
|
||||
explicit: Final = get_explicit_chatgpt_session_id(litellm_params)
|
||||
if explicit:
|
||||
return explicit
|
||||
params: Final = _normalize_litellm_params(litellm_params)
|
||||
return next(
|
||||
(str(value) for key in ("litellm_trace_id", "litellm_call_id") if (value := params.get(key))),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def ensure_chatgpt_session_id(litellm_params: object) -> str:
|
||||
return get_chatgpt_session_id(litellm_params) or str(uuid4())
|
||||
|
||||
|
||||
def should_derive_chatgpt_session_id(litellm_params: object) -> bool:
|
||||
value: Final = _normalize_litellm_params(litellm_params).get("chatgpt_derive_session_id")
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() == "true"
|
||||
return value is True
|
||||
|
||||
|
||||
_INPUT_ITEMS: Final = TypeAdapter(tuple[object, ...])
|
||||
|
||||
|
||||
def _first_input_item(value: object) -> object:
|
||||
"""The first element of a list-shaped ``input``; any other shape hashes whole."""
|
||||
try:
|
||||
items: Final = _INPUT_ITEMS.validate_python(value)
|
||||
except ValidationError:
|
||||
return value
|
||||
return items[0] if items else value
|
||||
|
||||
|
||||
def derive_chatgpt_session_id(litellm_params: object, instructions: str | None, input: object) -> str:
|
||||
params: Final = _normalize_litellm_params(litellm_params)
|
||||
metadata: Final = _normalize_litellm_params(params.get("litellm_metadata") or params.get("metadata"))
|
||||
tenant: Final = str(metadata.get("user_api_key_hash") or "")
|
||||
first_item: Final = _first_input_item(input)
|
||||
anchor: Final = json.dumps(
|
||||
(tenant, instructions or "", first_item),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return f"litellm-derived-{sha256(anchor.encode('utf-8')).hexdigest()[:32]}"
|
||||
|
|
|
|||
|
|
@ -32,10 +32,13 @@ from ..authenticator import Authenticator, get_cached_authenticator, get_chatgpt
|
|||
from ..common_utils import (
|
||||
CHATGPT_API_BASE,
|
||||
GetAccessTokenError,
|
||||
derive_chatgpt_session_id,
|
||||
ensure_chatgpt_session_id,
|
||||
get_chatgpt_client_credential,
|
||||
get_chatgpt_default_headers,
|
||||
get_chatgpt_default_instructions,
|
||||
get_explicit_chatgpt_session_id,
|
||||
should_derive_chatgpt_session_id,
|
||||
strip_chatgpt_client_credential_headers,
|
||||
)
|
||||
|
||||
|
|
@ -123,13 +126,20 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
)
|
||||
base_instructions: Final = get_chatgpt_default_instructions()
|
||||
existing_instructions: Final = request.get("instructions")
|
||||
if existing_instructions:
|
||||
if base_instructions not in existing_instructions:
|
||||
request["instructions"] = f"{base_instructions}\n\n{existing_instructions}"
|
||||
else:
|
||||
request["instructions"] = base_instructions
|
||||
instructions: Final = (
|
||||
f"{base_instructions}\n\n{existing_instructions}"
|
||||
if existing_instructions and base_instructions not in existing_instructions
|
||||
else f"{existing_instructions}"
|
||||
if existing_instructions
|
||||
else base_instructions
|
||||
)
|
||||
request["instructions"] = instructions
|
||||
request["store"] = False
|
||||
request["stream"] = True
|
||||
should_derive: Final = should_derive_chatgpt_session_id(litellm_params)
|
||||
if should_derive and get_explicit_chatgpt_session_id(litellm_params) is None:
|
||||
derived: Final = derive_chatgpt_session_id(litellm_params, instructions, input)
|
||||
headers["session_id"] = derived # rebind-ok: must land in the dict validate_environment built
|
||||
include: Final = list(request.get("include") or [])
|
||||
if "reasoning.encrypted_content" not in include:
|
||||
include.append("reasoning.encrypted_content")
|
||||
|
|
|
|||
|
|
@ -266,6 +266,109 @@ class TestChatGPTResponsesAPITransformation:
|
|||
"function": {"name": "hello"},
|
||||
}
|
||||
|
||||
def _transform_headers(
|
||||
self, input: list[dict[str, str]], litellm_params: GenericLiteLLMParams
|
||||
) -> dict[str, str]:
|
||||
headers: dict[str, str] = {}
|
||||
ChatGPTResponsesAPIConfig().transform_responses_api_request(
|
||||
model="chatgpt/gpt-5.4",
|
||||
input=input,
|
||||
response_api_optional_request_params={},
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
)
|
||||
return headers
|
||||
|
||||
def test_derived_session_id_is_stable_as_conversation_grows(self):
|
||||
params = GenericLiteLLMParams(chatgpt_derive_session_id=True)
|
||||
first_turn = [{"role": "user", "content": "start of conversation"}]
|
||||
later_turn = first_turn + [
|
||||
{"role": "assistant", "content": "reply"},
|
||||
{"role": "user", "content": "follow-up"},
|
||||
]
|
||||
|
||||
headers_one = self._transform_headers(first_turn, params)
|
||||
headers_two = self._transform_headers(later_turn, params)
|
||||
|
||||
assert headers_one["session_id"].startswith("litellm-derived-")
|
||||
assert headers_one["session_id"] == headers_two["session_id"]
|
||||
|
||||
def test_derived_session_id_differs_across_conversations(self):
|
||||
params = GenericLiteLLMParams(chatgpt_derive_session_id=True)
|
||||
|
||||
headers_one = self._transform_headers(
|
||||
[{"role": "user", "content": "conversation a"}], params
|
||||
)
|
||||
headers_two = self._transform_headers(
|
||||
[{"role": "user", "content": "conversation b"}], params
|
||||
)
|
||||
|
||||
assert headers_one["session_id"] != headers_two["session_id"]
|
||||
|
||||
def test_explicit_session_id_wins_over_derivation(self):
|
||||
params = GenericLiteLLMParams(
|
||||
chatgpt_derive_session_id=True, litellm_session_id="explicit-1"
|
||||
)
|
||||
|
||||
headers = self._transform_headers([{"role": "user", "content": "hi"}], params)
|
||||
|
||||
assert "session_id" not in headers
|
||||
|
||||
def test_proxy_trace_id_does_not_block_derivation(self):
|
||||
params = GenericLiteLLMParams(
|
||||
chatgpt_derive_session_id=True,
|
||||
litellm_trace_id="b6c2977a-0652-43de-a9a6-165822f439f6",
|
||||
)
|
||||
headers = {"session_id": params.litellm_trace_id}
|
||||
|
||||
ChatGPTResponsesAPIConfig().transform_responses_api_request(
|
||||
model="chatgpt/gpt-5.4",
|
||||
input=[{"role": "user", "content": "hi"}],
|
||||
response_api_optional_request_params={},
|
||||
litellm_params=params,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
assert headers["session_id"].startswith("litellm-derived-")
|
||||
|
||||
def test_different_api_keys_derive_different_session_ids(self):
|
||||
input = [{"role": "user", "content": "shared prefix"}]
|
||||
params_a = GenericLiteLLMParams(
|
||||
chatgpt_derive_session_id=True,
|
||||
litellm_metadata={"user_api_key_hash": "key-a"},
|
||||
)
|
||||
params_b = GenericLiteLLMParams(
|
||||
chatgpt_derive_session_id=True,
|
||||
litellm_metadata={"user_api_key_hash": "key-b"},
|
||||
)
|
||||
|
||||
headers_a = self._transform_headers(input, params_a)
|
||||
headers_b = self._transform_headers(input, params_b)
|
||||
|
||||
assert headers_a["session_id"] != headers_b["session_id"]
|
||||
assert headers_a["session_id"] == self._transform_headers(input, params_a)["session_id"]
|
||||
|
||||
def test_quoted_false_string_does_not_enable_derivation(self):
|
||||
params = GenericLiteLLMParams(chatgpt_derive_session_id="false")
|
||||
|
||||
headers = self._transform_headers([{"role": "user", "content": "hi"}], params)
|
||||
|
||||
assert "session_id" not in headers
|
||||
|
||||
def test_quoted_true_string_enables_derivation(self):
|
||||
params = GenericLiteLLMParams(chatgpt_derive_session_id="true")
|
||||
|
||||
headers = self._transform_headers([{"role": "user", "content": "hi"}], params)
|
||||
|
||||
assert headers["session_id"].startswith("litellm-derived-")
|
||||
|
||||
def test_no_derivation_without_flag(self):
|
||||
headers = self._transform_headers(
|
||||
[{"role": "user", "content": "hi"}], GenericLiteLLMParams()
|
||||
)
|
||||
|
||||
assert "session_id" not in headers
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model_name", "response_model"),
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue