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:
devin-ai-integration[bot] 2026-07-17 19:53:15 -07:00 • committed by GitHub
parent c8b36dc1d4
commit 9b0a424000
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 136 additions and 0 deletions

View file

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

View file

@ -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 = {