mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(proxy): derive session id from Anthropic metadata.user_id for session affinity (#33723)
* fix(router): resolve Anthropic metadata session affinity Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): derive Anthropic session affinity metadata Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): support Anthropic metadata session objects Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): normalize Anthropic metadata user object Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
c8b36dc1d4
commit
9b0a424000
2 changed files with 136 additions and 0 deletions
|
|
@ -45,6 +45,7 @@ _EXPLICIT_SESSION_HEADERS = frozenset({"x-litellm-trace-id", "x-litellm-session-
|
|||
# Session-id values must be non-empty strings of alphanumerics, hyphens, or underscores
|
||||
# (covers UUIDs and most common session-id formats).
|
||||
_SESSION_ID_VALUE_RE = re.compile(r"^[a-zA-Z0-9_\-]{8,}$")
|
||||
_ANTHROPIC_SESSION_ID_VALUE_RE = re.compile(r"^[a-zA-Z0-9_\-]+$")
|
||||
|
||||
|
||||
def _sanitize_for_log(value: Any) -> str:
|
||||
|
|
@ -426,6 +427,30 @@ def get_chain_id_from_headers(headers: Optional[Dict[str, str]]) -> Optional[str
|
|||
)
|
||||
|
||||
|
||||
def _get_anthropic_session_id_from_metadata(metadata: object) -> str | None:
|
||||
if not isinstance(metadata, dict):
|
||||
return None
|
||||
|
||||
user_id = metadata.get("user_id")
|
||||
if isinstance(user_id, dict):
|
||||
session_id = user_id.get("session_id")
|
||||
if isinstance(session_id, str) and _ANTHROPIC_SESSION_ID_VALUE_RE.fullmatch(session_id):
|
||||
return session_id
|
||||
return None
|
||||
if not isinstance(user_id, str):
|
||||
return None
|
||||
|
||||
session_marker = "_session_"
|
||||
session_marker_index = user_id.rfind(session_marker)
|
||||
if session_marker_index == -1:
|
||||
return None
|
||||
|
||||
session_id = user_id[session_marker_index + len(session_marker) :]
|
||||
if not session_id or not _ANTHROPIC_SESSION_ID_VALUE_RE.fullmatch(session_id):
|
||||
return None
|
||||
return session_id
|
||||
|
||||
|
||||
def is_claude_code_user_agent(user_agent: str) -> bool:
|
||||
"""Claude Code identifies itself as ``claude-cli/<version> ...``; the IDE
|
||||
extensions and the Agent SDK run through the same CLI and share that prefix."""
|
||||
|
|
@ -935,6 +960,15 @@ class LiteLLMProxyRequestSetup:
|
|||
data["litellm_session_id"] = chain_id
|
||||
data["litellm_trace_id"] = chain_id
|
||||
verbose_proxy_logger.debug(f"Extracted chain_id from header (trace-id/session-id): {chain_id}")
|
||||
else:
|
||||
body_metadata = data.get("metadata")
|
||||
session_id = _get_anthropic_session_id_from_metadata(body_metadata)
|
||||
if session_id:
|
||||
metadata_from_headers["session_id"] = session_id
|
||||
data["litellm_session_id"] = session_id
|
||||
if isinstance(body_metadata, dict) and isinstance(body_metadata.get("user_id"), dict):
|
||||
body_metadata["user_id"] = session_id
|
||||
verbose_proxy_logger.debug("Extracted session_id from Anthropic metadata.user_id")
|
||||
|
||||
if isinstance(data[_metadata_variable_name], dict):
|
||||
data[_metadata_variable_name].update(metadata_from_headers)
|
||||
|
|
|
|||
|
|
@ -2560,6 +2560,108 @@ def test_add_litellm_metadata_from_request_headers_generic_session_id_header():
|
|||
assert data["litellm_trace_id"] == "e96634a3-fa28-4083-b354-55542e2dca01"
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_anthropic_user_id_sets_session_id():
|
||||
data = {
|
||||
"metadata": {
|
||||
"user_id": "user_abc123_account__session_e96634a3-fa28-4083-b354-55542e2dca01"
|
||||
}
|
||||
}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers={}, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data["metadata"]["session_id"] == "e96634a3-fa28-4083-b354-55542e2dca01"
|
||||
assert data["litellm_session_id"] == "e96634a3-fa28-4083-b354-55542e2dca01"
|
||||
assert "litellm_trace_id" not in data
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_anthropic_user_id_dict_sets_session_id():
|
||||
data = {
|
||||
"metadata": {
|
||||
"user_id": {
|
||||
"device_id": "device",
|
||||
"account_uuid": "account",
|
||||
"session_id": "sess_4f8c1d2a-1234",
|
||||
}
|
||||
}
|
||||
}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers={}, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data["metadata"]["user_id"] == "sess_4f8c1d2a-1234"
|
||||
assert data["metadata"]["session_id"] == "sess_4f8c1d2a-1234"
|
||||
assert data["litellm_session_id"] == "sess_4f8c1d2a-1234"
|
||||
assert "litellm_trace_id" not in data
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_headers_session_id_beats_anthropic_user_id():
|
||||
data = {
|
||||
"metadata": {
|
||||
"user_id": "user_abc123_account__session_body-session-id",
|
||||
}
|
||||
}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers={"x-litellm-session-id": "header-session-id"},
|
||||
data=data,
|
||||
_metadata_variable_name="metadata",
|
||||
)
|
||||
assert data["metadata"]["session_id"] == "header-session-id"
|
||||
assert data["litellm_session_id"] == "header-session-id"
|
||||
assert data["litellm_trace_id"] == "header-session-id"
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_headers_session_id_beats_anthropic_user_id_dict():
|
||||
data = {
|
||||
"metadata": {
|
||||
"user_id": {
|
||||
"session_id": "body-session-id",
|
||||
}
|
||||
}
|
||||
}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers={"x-litellm-session-id": "header-session-id"},
|
||||
data=data,
|
||||
_metadata_variable_name="metadata",
|
||||
)
|
||||
assert data["metadata"]["session_id"] == "header-session-id"
|
||||
assert data["litellm_session_id"] == "header-session-id"
|
||||
assert data["litellm_trace_id"] == "header-session-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_id",
|
||||
[
|
||||
"user_abc123_account__session_",
|
||||
"user_abc123_account_",
|
||||
"user_abc123_account__session_invalid!",
|
||||
],
|
||||
)
|
||||
def test_add_litellm_metadata_from_anthropic_user_id_ignores_invalid_session_id(user_id: str):
|
||||
data = {"metadata": {"user_id": user_id}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers={}, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data == {"metadata": {"user_id": user_id}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_id",
|
||||
[
|
||||
{},
|
||||
{"session_id": 123},
|
||||
{"session_id": "invalid session id"},
|
||||
{"session_id": ""},
|
||||
],
|
||||
)
|
||||
def test_add_litellm_metadata_from_anthropic_user_id_dict_ignores_invalid_session_id(
|
||||
user_id: object,
|
||||
):
|
||||
data = {"metadata": {"user_id": user_id}}
|
||||
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
|
||||
headers={}, data=data, _metadata_variable_name="metadata"
|
||||
)
|
||||
assert data == {"metadata": {"user_id": user_id}}
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_explicit_header_beats_generic():
|
||||
"""Explicit x-litellm-trace-id wins over a generic x-*-session-id header."""
|
||||
headers = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue