From 8fd404c68b874ba265705f10a41eb21b649d97ba Mon Sep 17 00:00:00 2001 From: Etienne Chabert Date: Mon, 17 Aug 2026 22:46:07 +0200 Subject: [PATCH] fix(proxy): derive session id from vendor-less x-session-id header get_chain_id_from_headers honored x-litellm-trace-id, x-litellm-session-id and any vendor-scoped x--session-id (e.g. x-claude-code-session-id), but a bare x-session-id never matched the vendor regex, so clients sending one (e.g. opencode, which sends X-Session-Id on every request) fell back to a random SpendLogs.session_id per request and per-session spend views could not group their conversation. Accept a vendor-less x-session-id as a chain-id source, checked after the vendor-scoped headers with the same value rules, so a more specific header (e.g. opencode's x-parent-session-id on subagent calls) still wins. --- litellm/proxy/litellm_pre_call_utils.py | 21 ++++++++ .../proxy/test_litellm_pre_call_utils.py | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 4794da05a3e..be1f27d6193 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -621,6 +621,23 @@ def _extract_codex_session_id_from_headers( ) +def _extract_bare_session_id_from_headers( + normalized: Mapping[str, str], +) -> str | None: + """ + Return a vendor-less ``x-session-id`` header value (e.g. opencode) when it + is a plausible session/trace identifier (same value rules as the + vendor-scoped scan). + + Checked after the ``x--session-id`` headers so a more specific + header (e.g. opencode's ``x-parent-session-id`` on subagent calls) wins. + """ + value: Final = normalized.get("x-session-id") + if isinstance(value, str) and _SESSION_ID_VALUE_RE.match(value): + return value + return None + + def get_chain_id_from_headers(headers: dict[str, str] | None) -> str | None: """ Extract chain id for call chaining from request headers. @@ -631,6 +648,9 @@ def get_chain_id_from_headers(headers: dict[str, str] | None) -> str | None: 3. Any ``x--session-id`` header whose value looks like a session id (alphanumeric / UUID, at least 8 chars). E.g. ``x-claude-code-session-id``. 4. Codex's unprefixed ``session-id`` / ``thread-id``, for Codex callers only. + 5. A vendor-less ``x-session-id`` header (e.g. opencode), with the same + value rules. Checked last so a more specific vendor-scoped header + (e.g. opencode's ``x-parent-session-id`` on subagent calls) still wins. Header keys are matched case-insensitively so this works with raw header dicts from any transport. @@ -646,6 +666,7 @@ def get_chain_id_from_headers(headers: dict[str, str] | None) -> str | None: or normalized.get("x-litellm-session-id") or _extract_generic_session_id_from_headers(normalized) or _extract_codex_session_id_from_headers(normalized) + or _extract_bare_session_id_from_headers(normalized) ) diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 81a97a70efa..6a11d9e1590 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -3263,6 +3263,20 @@ def test_add_litellm_metadata_from_request_headers_explicit_header_beats_generic assert data["litellm_trace_id"] == "explicit-trace-id-value" +def test_add_litellm_metadata_from_request_headers_bare_session_id(): + """A vendor-less x-session-id header (e.g. opencode) lands on litellm_session_id.""" + data = {"metadata": {}} + LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers( + headers={"X-Session-Id": "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J"}, + data=data, + _metadata_variable_name="metadata", + ) + assert data["litellm_session_id"] == "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J" + assert data["litellm_trace_id"] == "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J" + assert data["metadata"]["session_id"] == "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J" + assert data["metadata"]["trace_id"] == "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J" + + def test_get_chain_id_from_headers_generic_vendor_session_id(): """get_chain_id_from_headers picks up any x--session-id with a valid value.""" from litellm.proxy.litellm_pre_call_utils import get_chain_id_from_headers @@ -3411,6 +3425,40 @@ def test_add_litellm_metadata_groups_codex_turns_into_one_session(): assert turn["litellm_metadata"]["session_id"] == CODEX_SESSION_UUID +def test_get_chain_id_from_headers_bare_session_id(): + """get_chain_id_from_headers picks up a vendor-less x-session-id (e.g. opencode).""" + from litellm.proxy.litellm_pre_call_utils import get_chain_id_from_headers + + assert ( + get_chain_id_from_headers({"x-session-id": "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J"}) + == "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J" + ) + assert ( + get_chain_id_from_headers({"X-Session-Id": "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J"}) + == "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J" + ) + assert get_chain_id_from_headers({"x-session-id": "short"}) is None + assert get_chain_id_from_headers({"x-session-id": "has spaces!!"}) is None + assert ( + get_chain_id_from_headers( + { + "x-litellm-session-id": "explicit-id-value", + "x-session-id": "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J", + } + ) + == "explicit-id-value" + ) + assert ( + get_chain_id_from_headers( + { + "x-session-id": "ses_01J5Z8K2M4NQRA6BCD8E9FGH0J", + "x-parent-session-id": "e96634a3-fa28-4083-b354-55542e2dca01", + } + ) + == "e96634a3-fa28-4083-b354-55542e2dca01" + ) + + def test_trace_id_from_traceparent_valid(): from litellm.proxy.litellm_pre_call_utils import _trace_id_from_traceparent