mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
Following on the heels of #35293 TODO: - Packages outside of this repo (e.g. LiteLLM, Nvidia, Google, AWS) --- ## Summary Surface partner package versions in `metadata.versions` on LangSmith traces. Mirrors the JS SDK's `_addVersion()` pattern ([langchainjs#10106](https://github.com/langchain-ai/langchainjs/pull/10106)). Each model constructor records its package version via `_add_version()` on `BaseLanguageModel`. The version dict accumulates through the class hierarchy — `langchain-core` is added in `BaseLanguageModel.model_post_init`, `langchain-openai` in `BaseChatOpenAI._set_openai_chat_version`, and each leaf partner in its uniquely-named `model_validator`. Traces end up with: ```json { "metadata": { "versions": { "langchain-core": "1.4.5", "langchain-openai": "1.3.0", "langchain-xai": "1.2.2" } } } ``` ### Changes - `BaseLanguageModel._add_version(pkg, version)` — appends to `self.metadata["versions"]`; accepts any `Mapping` type; emits a warning if a non-mapping value is found and replaced - `BaseLanguageModel.model_post_init` — adds `langchain-core` version; calls `super()` for MRO safety - `_merge_metadata_dicts` — one-level-deep (non-recursive) merge for nested dict metadata keys - `CallbackManager.add_metadata` — uses `_merge_metadata_dicts` instead of flat `dict.update()` so nested metadata dicts (like `versions`) coexist rather than clobber - `merge_configs` — uses `_merge_metadata_dicts` for config merging **Partners:** - Each now calls `self._add_version("langchain-<pkg>", __version__)` ### Design decisions - **Constructor-based, not `_get_ls_params`-based** — versions flow through `self.metadata` (local metadata on traces), not through `LangSmithParams`. This matches JS and makes child-class version inheritance automatic (no merge/clobber issues). - **`versions` is local (non-inheritable) metadata** — `self.metadata` is passed to `CallbackManager.configure` as `local_metadata` (`add_metadata(..., inherit=False)`), so `versions` is attached **once per chat-model run** and is **not** propagated to child runs or duplicated onto every streaming chunk. This is intentionally the opposite of the inheritable-per-chunk metadata that #36588 was reducing for performance — `versions` does not regress that path. - **`add_metadata` deep-merge is a correctness fix, not just for versions** — previously `add_metadata`/`merge_configs` did a flat top-level `dict.update`/spread, so any nested metadata dict baked into a config (e.g. via `.with_config({"metadata": {...}})`) would be wholly replaced when a caller also passed `metadata`. `_merge_metadata_dicts` merges one level deep so user-provided `config.metadata.versions` and model-set `versions` coexist instead of clobbering. The merge runs once per `configure` (not per chunk), so it is off the streaming hot path. - **One level deep only** — `_merge_metadata_dicts` is deliberately *not* a recursive deep merge; values nested more than one level are last-writer-wins. This covers the `versions` case without the ambiguity/cost of arbitrary-depth merging. - **Warn on non-dict `metadata["versions"]`** — if a user sets `metadata={"versions": "some-string"}`, `_add_version` emits a warning and replaces the value with the version dict rather than silently discarding user data or crashing. This is a soft breaking change for anyone who previously stored non-dict values at this key. ### Follow-ups (tracked separately, out of scope here) - JS `mergeConfigs` still flat-spreads nested metadata, so `metadata.versions` can still clobber on the JS side until an equivalent deep-merge lands. --- Made by [Open SWE](https://openswe.vercel.app) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""Unit tests for `scripts/check_version.py`.
|
|
|
|
The version-consistency check is duplicated verbatim across partner packages, so
|
|
exercising the parsing helpers here covers the shared logic. The script is loaded
|
|
by path because `scripts/` is not an importable package.
|
|
"""
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
|
|
import pytest
|
|
|
|
_SCRIPT_PATH = Path(__file__).parents[2] / "scripts" / "check_version.py"
|
|
|
|
|
|
def _load_script() -> ModuleType:
|
|
spec = importlib.util.spec_from_file_location("_check_version", _SCRIPT_PATH)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
check_version = _load_script()
|
|
|
|
|
|
def test_get_pyproject_version_parses_version(tmp_path: Path) -> None:
|
|
pyproject = tmp_path / "pyproject.toml"
|
|
pyproject.write_text('[project]\nname = "x"\nversion = "1.2.3"\n', encoding="utf-8")
|
|
assert check_version.get_pyproject_version(pyproject) == "1.2.3"
|
|
|
|
|
|
def test_get_pyproject_version_missing_returns_none(tmp_path: Path) -> None:
|
|
pyproject = tmp_path / "pyproject.toml"
|
|
pyproject.write_text('[project]\nname = "x"\n', encoding="utf-8")
|
|
assert check_version.get_pyproject_version(pyproject) is None
|
|
|
|
|
|
def test_get_version_py_version_parses_version(tmp_path: Path) -> None:
|
|
version_py = tmp_path / "_version.py"
|
|
version_py.write_text('__version__ = "4.5.6"\n', encoding="utf-8")
|
|
assert check_version.get_version_py_version(version_py) == "4.5.6"
|
|
|
|
|
|
def test_get_version_py_version_missing_returns_none(tmp_path: Path) -> None:
|
|
version_py = tmp_path / "_version.py"
|
|
version_py.write_text('"""No version here."""\n', encoding="utf-8")
|
|
assert check_version.get_version_py_version(version_py) is None
|
|
|
|
|
|
def test_main_passes_for_real_package() -> None:
|
|
"""The shipped `pyproject.toml` and `_version.py` must already agree."""
|
|
assert check_version.main() == 0
|
|
|
|
|
|
def test_main_reports_mismatch(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A version mismatch must fail loudly with a non-zero exit code."""
|
|
monkeypatch.setattr(check_version, "get_pyproject_version", lambda _: "1.0.0")
|
|
monkeypatch.setattr(check_version, "get_version_py_version", lambda _: "2.0.0")
|
|
assert check_version.main() == 1
|