fix(proxy): recognize opencode's bare x-session-id header for session affinity (#39802)

Co-authored-by: yassin <yassin@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-09-04 17:16:45 -07:00 committed by GitHub
parent b3c867c7b2
commit c373645e21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 0 deletions

View file

@ -669,6 +669,21 @@ def _extract_codex_session_id_from_headers(
)
def _extract_bare_session_id_from_headers(
normalized: Mapping[str, str],
) -> str | None:
"""
Read a vendor-less ``x-session-id`` header (opencode sends ``X-Session-Id``
alongside ``x-session-affinity`` on every turn of a session). Checked after
the ``x-<vendor>-session-id`` scan so a more specific header such as
opencode's ``x-parent-session-id`` on subagent calls keeps winning.
"""
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.
@ -679,6 +694,7 @@ def get_chain_id_from_headers(headers: dict[str, str] | None) -> str | None:
3. Any ``x-<vendor>-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), same value rules.
Header keys are matched case-insensitively so this works with raw header
dicts from any transport.
@ -694,6 +710,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)
)

View file

@ -3343,6 +3343,62 @@ def test_add_litellm_metadata_groups_codex_turns_into_one_session():
assert turn["litellm_metadata"]["session_id"] == CODEX_SESSION_UUID
OPENCODE_SESSION_ID = "ses_f91e6e825ffeuhlu5EbglxjAN2"
OPENCODE_HEADERS = {
"x-session-affinity": OPENCODE_SESSION_ID,
"X-Session-Id": OPENCODE_SESSION_ID,
"User-Agent": "opencode/1.18.28",
}
def test_add_litellm_metadata_groups_opencode_turns_into_one_session():
"""Every turn of an opencode session must land on metadata.session_id, which is what
DeploymentAffinityCheck reads for session pinning, instead of a fresh per-call id."""
turns = [{"metadata": {}}, {"metadata": {}}]
for turn in turns:
LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
headers=OPENCODE_HEADERS, data=turn, _metadata_variable_name="metadata"
)
for turn in turns:
assert turn["metadata"]["session_id"] == OPENCODE_SESSION_ID
assert turn["metadata"]["trace_id"] == OPENCODE_SESSION_ID
assert turn["litellm_session_id"] == OPENCODE_SESSION_ID
assert turn["litellm_trace_id"] == OPENCODE_SESSION_ID
@pytest.mark.parametrize("value", ["short", "has spaces!!", ""])
def test_get_chain_id_from_headers_bare_session_id_ignores_implausible_value(value: str):
from litellm.proxy.litellm_pre_call_utils import get_chain_id_from_headers
assert get_chain_id_from_headers({"x-session-id": value}) is None
@pytest.mark.parametrize(
"other_header",
[
"x-litellm-trace-id",
"x-litellm-session-id",
"x-claude-code-session-id",
"x-parent-session-id",
],
)
def test_get_chain_id_from_headers_bare_session_id_loses_to_more_specific_header(other_header: str):
"""opencode subagent calls carry x-parent-session-id next to X-Session-Id; explicit and
vendor-scoped headers must keep winning over the bare header."""
from litellm.proxy.litellm_pre_call_utils import get_chain_id_from_headers
assert (
get_chain_id_from_headers(
{
"x-session-id": OPENCODE_SESSION_ID,
other_header: "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