From d7bca50323650932505556219e23b61a0d6e35fc Mon Sep 17 00:00:00 2001 From: Hari Harish Date: Wed, 24 Jun 2026 14:22:29 -0700 Subject: [PATCH] stop overriding caller's ls_integration in ls trace --- .../language_models/chat_models.py | 72 ++++++++++++------- .../language_models/chat_models/test_base.py | 12 ++++ libs/langchain_v1/langchain/agents/factory.py | 5 +- .../test_injected_runtime_create_agent.py | 3 +- 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/libs/core/langchain_core/language_models/chat_models.py b/libs/core/langchain_core/language_models/chat_models.py index 01737308ebe..54c4e24dc43 100644 --- a/libs/core/langchain_core/language_models/chat_models.py +++ b/libs/core/langchain_core/language_models/chat_models.py @@ -738,10 +738,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): params = self._get_invocation_params(stop=stop, **kwargs) options = {"stop": stop, **kwargs, **ls_structured_output_format_dict} - inheritable_metadata = { - **(config.get("metadata") or {}), - **self._get_ls_params_with_defaults(stop=stop, **kwargs), - } + inheritable_metadata = self._merge_inheritable_metadata( + config.get("metadata") or {}, + self._get_ls_params_with_defaults(stop=stop, **kwargs), + ) callback_manager = CallbackManager.configure( config.get("callbacks"), self.callbacks, @@ -867,10 +867,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): params = self._get_invocation_params(stop=stop, **kwargs) options = {"stop": stop, **kwargs, **ls_structured_output_format_dict} - inheritable_metadata = { - **(config.get("metadata") or {}), - **self._get_ls_params_with_defaults(stop=stop, **kwargs), - } + inheritable_metadata = self._merge_inheritable_metadata( + config.get("metadata") or {}, + self._get_ls_params_with_defaults(stop=stop, **kwargs), + ) callback_manager = AsyncCallbackManager.configure( config.get("callbacks"), self.callbacks, @@ -1002,10 +1002,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): params = self._get_invocation_params(stop=stop, **kwargs) options = {"stop": stop, **kwargs, **ls_structured_output_format_dict} - inheritable_metadata = { - **(config.get("metadata") or {}), - **self._get_ls_params_with_defaults(stop=stop, **kwargs), - } + inheritable_metadata = self._merge_inheritable_metadata( + config.get("metadata") or {}, + self._get_ls_params_with_defaults(stop=stop, **kwargs), + ) callback_manager = CallbackManager.configure( config.get("callbacks"), self.callbacks, @@ -1136,10 +1136,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): params = self._get_invocation_params(stop=stop, **kwargs) options = {"stop": stop, **kwargs, **ls_structured_output_format_dict} - inheritable_metadata = { - **(config.get("metadata") or {}), - **self._get_ls_params_with_defaults(stop=stop, **kwargs), - } + inheritable_metadata = self._merge_inheritable_metadata( + config.get("metadata") or {}, + self._get_ls_params_with_defaults(stop=stop, **kwargs), + ) callback_manager = AsyncCallbackManager.configure( config.get("callbacks"), self.callbacks, @@ -1547,6 +1547,30 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): ls_params["ls_integration"] = "langchain_chat_model" return ls_params + @staticmethod + def _merge_inheritable_metadata( + inherited: dict[str, Any], ls_params: LangSmithParams + ) -> dict[str, Any]: + """Merge model `ls_params` into inherited run metadata. + + Replacing "last-writer-wins" policy for `ls_integration`. + If a caller already set one on the run metadata, keep it instead of + overriding with the chat model default. + Mirrors langgraph, which sets `ls_integration` only when it is absent. + + Args: + inherited: Run metadata inherited from the invocation config. + ls_params: LangSmith params for this chat model, including the + default `ls_integration`. + + Returns: + The merged metadata, preserving any inherited `ls_integration`. + """ + merged: dict[str, Any] = {**inherited, **ls_params} + if inherited.get("ls_integration"): + merged["ls_integration"] = inherited["ls_integration"] + return merged + def _get_llm_string(self, stop: list[str] | None = None, **kwargs: Any) -> str: if self.is_lc_serializable(): params = {**kwargs, "stop": stop} @@ -1617,10 +1641,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): params = self._get_invocation_params(stop=stop, **kwargs) options = {"stop": stop, **ls_structured_output_format_dict} - inheritable_metadata = { - **(metadata or {}), - **self._get_ls_params_with_defaults(stop=stop, **kwargs), - } + inheritable_metadata = self._merge_inheritable_metadata( + metadata or {}, + self._get_ls_params_with_defaults(stop=stop, **kwargs), + ) callback_manager = CallbackManager.configure( callbacks, @@ -1743,10 +1767,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC): params = self._get_invocation_params(stop=stop, **kwargs) options = {"stop": stop, **ls_structured_output_format_dict} - inheritable_metadata = { - **(metadata or {}), - **self._get_ls_params_with_defaults(stop=stop, **kwargs), - } + inheritable_metadata = self._merge_inheritable_metadata( + metadata or {}, + self._get_ls_params_with_defaults(stop=stop, **kwargs), + ) callback_manager = AsyncCallbackManager.configure( callbacks, diff --git a/libs/core/tests/unit_tests/language_models/chat_models/test_base.py b/libs/core/tests/unit_tests/language_models/chat_models/test_base.py index f8097938965..07044107013 100644 --- a/libs/core/tests/unit_tests/language_models/chat_models/test_base.py +++ b/libs/core/tests/unit_tests/language_models/chat_models/test_base.py @@ -1362,6 +1362,18 @@ async def test_user_lc_versions_metadata_survives_merge_async() -> None: assert "langchain-core" in run_metadata["lc_versions"] +def test_caller_ls_integration_survives_merge() -> None: + """A caller-supplied `ls_integration` is not overridden by the model default.""" + llm = FakeListChatModel(responses=["hello"]) + user_config: RunnableConfig = {"metadata": {"ls_integration": "my-integration"}} + + with collect_runs() as cb: + llm.invoke([HumanMessage(content="hi")], config=user_config) + assert len(cb.traced_runs) == 1 + run_metadata = cb.traced_runs[0].extra["metadata"] + assert run_metadata["ls_integration"] == "my-integration" + + def test_user_lc_versions_metadata_survives_merge_stream() -> None: """Stream variant: user-provided `lc_versions` metadata merges with model's.""" llm = _VersionedFakeModel(responses=["hello"]) diff --git a/libs/langchain_v1/langchain/agents/factory.py b/libs/langchain_v1/langchain/agents/factory.py index 679f207a0d0..bc35d51d22e 100644 --- a/libs/langchain_v1/langchain/agents/factory.py +++ b/libs/langchain_v1/langchain/agents/factory.py @@ -1778,9 +1778,10 @@ def create_agent( # Set recursion limit to 9_999 # https://github.com/langchain-ai/langgraph/issues/7313 config: RunnableConfig = {"recursion_limit": 9_999} - config["metadata"] = {"ls_integration": "langchain_create_agent"} + # Don't bake ls_integration here: it would override an outer integration's + # value inherited ambiently through the parent run tree. if name: - config["metadata"]["lc_agent_name"] = name + config["metadata"] = {"lc_agent_name": name} middleware_transformers = [t for m in middleware for t in getattr(m, "transformers", ())] diff --git a/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py b/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py index f9d13afc322..1ba93755ed5 100644 --- a/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py +++ b/libs/langchain_v1/tests/unit_tests/agents/test_injected_runtime_create_agent.py @@ -279,7 +279,8 @@ def test_tool_runtime_config_access() -> None: assert config_data["config_exists"] is True assert "config_keys" in config_data assert config_data["recursion_limit"] == 9999 - assert config_data["metadata"]["ls_integration"] == "langchain_create_agent" + # create_agent no longer bakes ls_integration; it defers to the caller. + assert "ls_integration" not in (config_data["metadata"] or {}) tool_message = result["messages"][2] assert isinstance(tool_message, ToolMessage)