mirror of
https://github.com/hwchase17/langchain.git
synced 2026-05-03 09:56:00 +00:00
feat(openrouter): session_id and trace fields (#37137)
Add first-class `session_id` and `trace` constructor fields on `ChatOpenRouter`, plumbed into the request payload alongside the existing `route` / `plugins` / `openrouter_provider` knobs. Both correspond to the OpenRouter [broadcast](https://openrouter.ai/docs/guides/features/broadcast/overview) feature for grouping requests under one logical workflow and attaching per-request observability metadata. Previously these were only reachable by stuffing them through `model_kwargs` or `.bind()`, neither of which is discoverable. [Docs](https://github.com/langchain-ai/docs/pull/3840) ## Changes - New `session_id: str | None` field with a `from_env("OPENROUTER_SESSION_ID", default=None)` factory, so a process can group all requests via env var without threading the value through application code. Truthy-guarded in `_default_params` so an explicit or env-sourced empty string is treated as unset. - New `trace: dict[str, Any] | None` field for per-request trace metadata (`trace_id`, `trace_name`, `span_name`, `generation_name`, `parent_span_id`, plus arbitrary extras forwarded as custom metadata). No env fallback — set per-call or on the constructor. - Per-call override (`model.invoke(..., session_id=..., trace=...)`) works through the existing `**kwargs` flow into `_generate` / `_stream`, with the constructor value preserved across calls. - Updated the "Key init args — client params" docstring table on `ChatOpenRouter` to surface both fields.
This commit is contained in:
@@ -119,6 +119,8 @@ class ChatOpenRouter(BaseChatModel):
|
||||
| `app_url` | `str | None` | App URL for attribution. |
|
||||
| `app_title` | `str | None` | App title for attribution. |
|
||||
| `app_categories` | `list[str] | None` | Marketplace attribution categories. |
|
||||
| `session_id` | `str | None` | Group related requests for observability. |
|
||||
| `trace` | `dict[str, Any] | None` | Trace metadata for broadcasts. |
|
||||
| `max_retries` | `int` | Max retries (default `2`). Set to `0` to disable. |
|
||||
|
||||
??? info "Instantiate"
|
||||
@@ -292,6 +294,42 @@ class ChatOpenRouter(BaseChatModel):
|
||||
plugins: list[dict[str, Any]] | None = None
|
||||
"""Plugins configuration for OpenRouter."""
|
||||
|
||||
session_id: str | None = Field(
|
||||
default_factory=from_env("OPENROUTER_SESSION_ID", default=None),
|
||||
)
|
||||
"""Identifier used by OpenRouter to group related requests together.
|
||||
|
||||
Useful any time multiple requests should share an observability
|
||||
grouping (e.g. a conversation, an agent workflow, a batch job, or a CI
|
||||
run). Equivalent to setting the `x-session-id` HTTP header on the
|
||||
underlying request. OpenRouter rejects values longer than 128
|
||||
characters.
|
||||
|
||||
Falls back to the `OPENROUTER_SESSION_ID` environment variable when
|
||||
unset, so callers can group all requests from a process without
|
||||
threading the value through application code. Empty strings are
|
||||
treated as unset.
|
||||
|
||||
Example: `"conv-2026-04-30-abc"`
|
||||
|
||||
See https://openrouter.ai/docs/guides/features/broadcast/overview
|
||||
"""
|
||||
|
||||
trace: dict[str, Any] | None = None
|
||||
"""Trace metadata for observability tools (e.g. Langfuse, LangSmith).
|
||||
|
||||
Forwarded by OpenRouter to configured broadcast destinations. Common
|
||||
keys include `trace_id`, `trace_name`, `span_name`, `generation_name`,
|
||||
and `parent_span_id`; see the OpenRouter broadcast docs for the
|
||||
current full set. Unknown keys are forwarded as custom metadata.
|
||||
|
||||
No environment-variable fallback — set per-call or on the constructor.
|
||||
|
||||
Example: `{"trace_id": "abc-123", "span_name": "summarize"}`
|
||||
|
||||
See https://openrouter.ai/docs/guides/features/broadcast/overview
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@@ -703,6 +741,10 @@ class ChatOpenRouter(BaseChatModel):
|
||||
params["route"] = self.route
|
||||
if self.plugins is not None:
|
||||
params["plugins"] = self.plugins
|
||||
if self.session_id:
|
||||
params["session_id"] = self.session_id
|
||||
if self.trace is not None:
|
||||
params["trace"] = self.trace
|
||||
return params
|
||||
|
||||
def _create_message_dicts(
|
||||
|
||||
@@ -757,6 +757,11 @@ class TestMockedGenerate:
|
||||
class TestRequestPayload:
|
||||
"""Tests verifying the exact dict sent to the SDK."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_openrouter_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Clear env vars that would otherwise leak into tests via `from_env`."""
|
||||
monkeypatch.delenv("OPENROUTER_SESSION_ID", raising=False)
|
||||
|
||||
def test_message_format_in_payload(self) -> None:
|
||||
"""Test that messages are formatted correctly in the SDK call."""
|
||||
model = _make_model(temperature=0)
|
||||
@@ -826,6 +831,115 @@ class TestRequestPayload:
|
||||
assert call_kwargs["provider"] == {"order": ["Anthropic"]}
|
||||
assert call_kwargs["route"] == "fallback"
|
||||
|
||||
def test_session_id_and_trace_in_payload(self) -> None:
|
||||
"""Test that session_id and trace are forwarded to the SDK."""
|
||||
model = _make_model(
|
||||
session_id="session-abc",
|
||||
trace={"trace_id": "trace-1", "span_name": "summarize"},
|
||||
)
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
|
||||
model.invoke("Hi")
|
||||
call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert call_kwargs["session_id"] == "session-abc"
|
||||
assert call_kwargs["trace"] == {
|
||||
"trace_id": "trace-1",
|
||||
"span_name": "summarize",
|
||||
}
|
||||
|
||||
def test_session_id_and_trace_omitted_when_unset(self) -> None:
|
||||
"""Test that session_id and trace are omitted when not configured."""
|
||||
model = _make_model()
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
|
||||
model.invoke("Hi")
|
||||
call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert "session_id" not in call_kwargs
|
||||
assert "trace" not in call_kwargs
|
||||
|
||||
def test_session_id_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test that session_id falls back to OPENROUTER_SESSION_ID env var."""
|
||||
monkeypatch.setenv("OPENROUTER_SESSION_ID", "env-session-xyz")
|
||||
model = _make_model()
|
||||
assert model.session_id == "env-session-xyz"
|
||||
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
model.invoke("Hi")
|
||||
call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert call_kwargs["session_id"] == "env-session-xyz"
|
||||
|
||||
def test_session_id_constructor_overrides_env(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that an explicit session_id wins over the env var."""
|
||||
monkeypatch.setenv("OPENROUTER_SESSION_ID", "env-session")
|
||||
model = _make_model(session_id="explicit-session")
|
||||
assert model.session_id == "explicit-session"
|
||||
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
model.invoke("Hi")
|
||||
call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert call_kwargs["session_id"] == "explicit-session"
|
||||
|
||||
def test_session_id_per_call_override(self) -> None:
|
||||
"""Test that a per-call session_id kwarg overrides the constructor value."""
|
||||
model = _make_model(session_id="constructor-session")
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
|
||||
model.invoke("Hi", session_id="call-session")
|
||||
first_call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert first_call_kwargs["session_id"] == "call-session"
|
||||
|
||||
# Per-call override must not mutate the constructor value, and the next
|
||||
# call without the kwarg should fall back to the constructor's value.
|
||||
assert model.session_id == "constructor-session"
|
||||
model.invoke("Hi")
|
||||
second_call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert second_call_kwargs["session_id"] == "constructor-session"
|
||||
|
||||
def test_trace_per_call_override(self) -> None:
|
||||
"""Test that a per-call trace kwarg overrides the constructor value."""
|
||||
constructor_trace = {"trace_id": "constructor-trace"}
|
||||
call_trace = {"trace_id": "call-trace", "span_name": "summarize"}
|
||||
model = _make_model(trace=constructor_trace)
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
|
||||
model.invoke("Hi", trace=call_trace)
|
||||
first_call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert first_call_kwargs["trace"] == call_trace
|
||||
|
||||
assert model.trace == constructor_trace
|
||||
model.invoke("Hi")
|
||||
second_call_kwargs = model.client.chat.send.call_args[1]
|
||||
assert second_call_kwargs["trace"] == constructor_trace
|
||||
|
||||
def test_empty_session_id_treated_as_unset(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that empty `session_id` (constructor or env) is not forwarded."""
|
||||
# Explicit empty string on the constructor.
|
||||
model = _make_model(session_id="")
|
||||
model.client = MagicMock()
|
||||
model.client.chat.send.return_value = _make_sdk_response(_SIMPLE_RESPONSE_DICT)
|
||||
model.invoke("Hi")
|
||||
assert "session_id" not in model.client.chat.send.call_args[1]
|
||||
|
||||
# Empty string sourced from the env var.
|
||||
monkeypatch.setenv("OPENROUTER_SESSION_ID", "")
|
||||
env_model = _make_model()
|
||||
env_model.client = MagicMock()
|
||||
env_model.client.chat.send.return_value = _make_sdk_response(
|
||||
_SIMPLE_RESPONSE_DICT
|
||||
)
|
||||
env_model.invoke("Hi")
|
||||
assert "session_id" not in env_model.client.chat.send.call_args[1]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# bind_tools tests
|
||||
|
||||
2
libs/partners/openrouter/uv.lock
generated
2
libs/partners/openrouter/uv.lock
generated
@@ -470,7 +470,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-tests"
|
||||
version = "1.1.6"
|
||||
version = "1.1.7"
|
||||
source = { editable = "../../standard-tests" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
Reference in New Issue
Block a user