feat(core,partners): add package version tracking to tracing metadata (#35295)

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>
This commit is contained in:
Mason Daughertyandopen-swe[bot] authored and GitHub committed 2026-06-11 02:23:19 -04:00
1 parent d5f7d33f88
commit 948f6cc58c
90 files changed
+1618 -173

No files matched your search

-67
View File
@@ -1,67 +0,0 @@
# Ensures version numbers in pyproject.toml and version.py stay in sync.
#
# (Prevents releases with mismatched version numbers)
name: "🔍 Check Version Equality"
on:
pull_request:
paths:
- "libs/core/pyproject.toml"
- "libs/core/langchain_core/version.py"
- "libs/partners/anthropic/pyproject.toml"
- "libs/partners/anthropic/langchain_anthropic/_version.py"
permissions:
contents: read
jobs:
check_version_equality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: "✅ Verify pyproject.toml & version.py Match"
run: |
# Check core versions
CORE_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/core/pyproject.toml)
CORE_VERSION_PY_VERSION=$(grep -Po '(?<=^VERSION = ")[^"]*' libs/core/langchain_core/version.py)
# Compare core versions
if [ "$CORE_PYPROJECT_VERSION" != "$CORE_VERSION_PY_VERSION" ]; then
echo "langchain-core versions in pyproject.toml and version.py do not match!"
echo "pyproject.toml version: $CORE_PYPROJECT_VERSION"
echo "version.py version: $CORE_VERSION_PY_VERSION"
exit 1
else
echo "Core versions match: $CORE_PYPROJECT_VERSION"
fi
# Check langchain_v1 versions
LANGCHAIN_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/langchain_v1/pyproject.toml)
LANGCHAIN_INIT_PY_VERSION=$(grep -Po '(?<=^__version__ = ")[^"]*' libs/langchain_v1/langchain/__init__.py)
# Compare langchain_v1 versions
if [ "$LANGCHAIN_PYPROJECT_VERSION" != "$LANGCHAIN_INIT_PY_VERSION" ]; then
echo "langchain_v1 versions in pyproject.toml and __init__.py do not match!"
echo "pyproject.toml version: $LANGCHAIN_PYPROJECT_VERSION"
echo "version.py version: $LANGCHAIN_INIT_PY_VERSION"
exit 1
else
echo "Langchain v1 versions match: $LANGCHAIN_PYPROJECT_VERSION"
fi
# Check langchain-anthropic versions
ANTHROPIC_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/partners/anthropic/pyproject.toml)
ANTHROPIC_VERSION_PY_VERSION=$(grep -Po '(?<=^__version__ = ")[^"]*' libs/partners/anthropic/langchain_anthropic/_version.py)
# Compare langchain-anthropic versions
if [ "$ANTHROPIC_PYPROJECT_VERSION" != "$ANTHROPIC_VERSION_PY_VERSION" ]; then
echo "langchain-anthropic versions in pyproject.toml and _version.py do not match!"
echo "pyproject.toml version: $ANTHROPIC_PYPROJECT_VERSION"
echo "_version.py version: $ANTHROPIC_VERSION_PY_VERSION"
exit 1
else
echo "Langchain-anthropic versions match: $ANTHROPIC_PYPROJECT_VERSION"
fi
+54
View File
@@ -0,0 +1,54 @@
# Ensures version numbers in pyproject.toml and _version.py stay in sync.
#
# (Prevents releases with mismatched version numbers)
name: "Check Version Equality"
on:
pull_request:
paths:
- "libs/core/pyproject.toml"
- "libs/core/langchain_core/version.py"
- "libs/langchain_v1/pyproject.toml"
- "libs/langchain_v1/langchain/__init__.py"
- "libs/partners/*/pyproject.toml"
- "libs/partners/**/_version.py"
permissions:
contents: read
jobs:
check_version_equality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: astral-sh/setup-uv@0ca8f610542aa7f4acaf39e65cf4eb3c35091883 # v7
with:
python-version: "3.12"
- name: "Verify pyproject.toml & version files match"
run: |
FAILED=0
for dir in libs/core libs/langchain_v1 libs/partners/*; do
[ -f "$dir/Makefile" ] || continue
if grep -q '^check_version:' "$dir/Makefile"; then
echo "--- $dir ---"
make -C "$dir" check_version || FAILED=1
elif find "$dir" -maxdepth 2 -name '_version.py' -not -path '*/tests/*' \
| grep -q .; then
# A package ships a _version.py but has no way to verify it stays
# in sync with pyproject.toml. Don't let it pass unchecked.
echo "--- $dir ---"
echo "Error: $dir has a _version.py but no 'check_version' Makefile target"
FAILED=1
fi
done
if [ "$FAILED" -ne 0 ]; then
echo ""
echo "One or more version checks failed!"
exit 1
fi
+8 -2
View File
@@ -1205,9 +1205,15 @@ class BaseCallbackManager(CallbackManagerMixin):
metadata: The metadata to add.
inherit: Whether to inherit the metadata.
"""
self.metadata.update(metadata)
from langchain_core.runnables.config import ( # noqa: PLC0415
_merge_metadata_dicts,
)
self.metadata = _merge_metadata_dicts(self.metadata, metadata)
if inherit:
self.inheritable_metadata.update(metadata)
self.inheritable_metadata = _merge_metadata_dicts(
self.inheritable_metadata, metadata
)
def remove_metadata(self, keys: list[str]) -> None:
"""Remove metadata from the callback manager.
@@ -138,6 +138,30 @@ def _get_verbosity() -> bool:
return get_verbose()
@cache
def _get_langchain_version() -> str | None:
"""Return the installed `langchain` version, or `None` if not installed.
Cached because `importlib.metadata.version` performs a filesystem lookup and
`model_post_init` runs on every `BaseLanguageModel` instantiation. `langchain`
is an optional sibling package, so its absence is expected and not an error.
"""
from importlib.metadata import PackageNotFoundError # noqa: PLC0415
from importlib.metadata import version as pkg_version # noqa: PLC0415
try:
return pkg_version("langchain")
except PackageNotFoundError:
return None
# Warm the cache at import time, while we're guaranteed to be on the synchronous
# import path and outside any event loop. Otherwise the first model constructed
# inside async code would run the blocking `os.stat` (via `importlib.metadata`)
# on the event loop, tripping blocking-I/O detectors like blockbuster.
_get_langchain_version()
class BaseLanguageModel(
RunnableSerializable[LanguageModelInput, LanguageModelOutputVar], ABC
):
@@ -179,6 +203,75 @@ class BaseLanguageModel(
arbitrary_types_allowed=True,
)
def model_post_init(self, _context: Any, /) -> None:
"""Pydantic V2 lifecycle hook called automatically after `__init__`.
Seeds `metadata["versions"]` with the installed `langchain-core`
(and `langchain`, if installed) versions so that every LLM trace
carries the package versions that produced it.
Partner packages should **not** override this method. Instead, they
should define a `@model_validator(mode="after")` that calls
`_add_version` to append their own version to the same dict.
!!! warning "Validator naming"
Each subclass's validator **must** have a unique name. Pydantic
replaces — rather than chains — same-named `model_validator` methods
in child classes. For example, a `BaseChatOpenAI` subclass should
use `_set_<partner>_version`, not `_set_version`, to avoid silently
dropping the parent's entry.
Args:
_context: Pydantic validation context (typically `None`).
"""
super().model_post_init(_context)
from langchain_core.version import VERSION # noqa: PLC0415
self._add_version("langchain-core", VERSION)
langchain_version = _get_langchain_version()
if langchain_version is not None:
self._add_version("langchain", langchain_version)
def _add_version(self, pkg: str, version: str) -> None:
"""Record a package version in `metadata.versions` for tracing.
Each layer in the class hierarchy (core -> langchain -> partner)
calls this so that the resulting metadata dict accumulates *all*
package versions involved in an invocation.
Example resulting metadata:
```python
{
"versions": {
"langchain-core": "1.x.x",
"langchain": "1.x.x",
"langchain-openai": "1.x.x",
}
}
```
Args:
pkg: Package name (e.g., `'langchain-openai'`).
version: Installed version string.
"""
if self.metadata is None:
self.metadata = {}
existing = self.metadata.get("versions")
if existing is not None and not isinstance(existing, Mapping):
warnings.warn(
f"metadata['versions'] expected a dict, got "
f"{type(existing).__name__}; overwriting with package version dict",
stacklevel=2,
)
existing = None
self.metadata["versions"] = {
**(existing if isinstance(existing, Mapping) else {}),
pkg: version,
}
@field_validator("verbose", mode="before")
def set_verbose(cls, verbose: bool | None) -> bool: # noqa: FBT001
"""If verbose is `None`, set it.
+57 -5
View File
@@ -7,7 +7,15 @@ import asyncio
# Cannot move uuid to TYPE_CHECKING as RunnableConfig is used in Pydantic models
import uuid # noqa: TC003
import warnings
from collections.abc import Awaitable, Callable, Generator, Iterable, Iterator, Sequence
from collections.abc import (
Awaitable,
Callable,
Generator,
Iterable,
Iterator,
Mapping,
Sequence,
)
from concurrent.futures import Executor, Future, ThreadPoolExecutor
from contextlib import contextmanager
from contextvars import Context, ContextVar, Token, copy_context
@@ -388,6 +396,50 @@ def patch_config(
return config
def _merge_metadata_dicts(
base: Mapping[str, Any], incoming: Mapping[str, Any]
) -> dict[str, Any]:
"""Merge two metadata dicts one level deep (not a recursive deep merge).
If both sides have a `Mapping` value for the same key, the inner mappings
are merged (last-writer-wins within). Non-mapping values use
last-writer-wins at the top level. Only one level of depth is merged; values
nested more deeply are not recursively merged.
Args:
base: The base metadata dict.
Values here are kept unless overridden by `incoming`.
incoming: The metadata dict to merge on top.
Its values take precedence on conflict.
Returns:
A new merged dict.
Inputs are not mutated. The returned dict performs shallow copies at
the top level and one level deep; mutable values nested beyond that
depth are shared references with the originals.
"""
merged = {**base}
for key, value in incoming.items():
if (
key in merged
and isinstance(merged[key], Mapping)
and isinstance(value, Mapping)
):
merged[key] = {**merged[key], **value}
elif isinstance(value, Mapping):
merged[key] = {**value}
else:
merged[key] = value
# Ensure non-overlapping nested mappings are also copies, not shared refs.
for key in base:
if key not in incoming and isinstance(merged[key], Mapping):
merged[key] = {**merged[key]}
return merged
def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
"""Merge multiple configs into one.
@@ -403,10 +455,10 @@ def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
for config in (ensure_config(c) for c in configs if c is not None):
for key in config:
if key == "metadata":
base["metadata"] = {
**base.get("metadata", {}),
**(config.get("metadata") or {}),
}
base["metadata"] = _merge_metadata_dicts(
base.get("metadata", {}),
config.get("metadata") or {},
)
elif key == "tags":
base["tags"] = sorted(
set(base.get("tags", []) + (config.get("tags") or [])),
@@ -4,6 +4,7 @@ import uuid
import warnings
from collections.abc import AsyncIterator, Iterator
from contextlib import contextmanager
from importlib.metadata import PackageNotFoundError
from typing import TYPE_CHECKING, Any, Literal, get_type_hints
from unittest.mock import patch
@@ -27,6 +28,7 @@ from langchain_core.language_models._utils import (
_filter_invocation_params_for_tracing,
_normalize_messages,
)
from langchain_core.language_models.base import _get_langchain_version
from langchain_core.language_models.chat_models import (
SimpleChatModel,
_generate_response_from_error,
@@ -52,6 +54,7 @@ from langchain_core.tracers.context import collect_runs
from langchain_core.tracers.event_stream import _AstreamEventsCallbackHandler
from langchain_core.tracers.langchain import LangChainTracer
from langchain_core.tracers.schemas import Run
from langchain_core.version import VERSION
from tests.unit_tests.fake.callbacks import (
BaseFakeCallbackHandler,
FakeAsyncCallbackHandler,
@@ -61,6 +64,7 @@ from tests.unit_tests.stubs import _any_id_ai_message, _any_id_ai_message_chunk
if TYPE_CHECKING:
from langchain_core.outputs.llm_result import LLMResult
from langchain_core.runnables.config import RunnableConfig
def _content_blocks_equal_ignore_id(
@@ -1305,6 +1309,187 @@ def test_get_ls_params() -> None:
assert ls_params["ls_stop"] == ["stop"]
class _VersionedFakeModel(FakeListChatModel):
"""Fake model that adds a version via `_add_version`."""
def model_post_init(self, _context: Any, /) -> None:
super().model_post_init(_context)
self._add_version("langchain-fake", "0.1.0")
def test_user_versions_metadata_survives_merge() -> None:
"""User-provided versions metadata should be deep-merged with model versions.
Regression test: the `add_metadata` deep-merge in `CallbackManager`
must preserve both user-provided and model-provided versions dicts.
"""
llm = _VersionedFakeModel(responses=["hello"])
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
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 "my-app" in run_metadata["versions"]
assert run_metadata["versions"]["my-app"] == "2.0"
assert "langchain-fake" in run_metadata["versions"]
assert "langchain-core" in run_metadata["versions"]
async def test_user_versions_metadata_survives_merge_async() -> None:
"""Async variant: user-provided versions metadata deep-merged with model's."""
llm = _VersionedFakeModel(responses=["hello"])
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
with collect_runs() as cb:
await llm.ainvoke([HumanMessage(content="hi")], config=user_config)
assert len(cb.traced_runs) == 1
run_metadata = cb.traced_runs[0].extra["metadata"]
assert "my-app" in run_metadata["versions"]
assert "langchain-fake" in run_metadata["versions"]
assert "langchain-core" in run_metadata["versions"]
def test_user_versions_metadata_survives_merge_stream() -> None:
"""Stream variant: user-provided versions metadata deep-merged with model's."""
llm = _VersionedFakeModel(responses=["hello"])
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
with collect_runs() as cb:
for _ in llm.stream([HumanMessage(content="hi")], config=user_config):
pass
assert len(cb.traced_runs) == 1
run_metadata = cb.traced_runs[0].extra["metadata"]
assert "my-app" in run_metadata["versions"]
assert "langchain-fake" in run_metadata["versions"]
assert "langchain-core" in run_metadata["versions"]
async def test_user_versions_metadata_survives_merge_astream() -> None:
"""Async stream variant: user-provided versions metadata deep-merged."""
llm = _VersionedFakeModel(responses=["hello"])
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
with collect_runs() as cb:
async for _ in llm.astream([HumanMessage(content="hi")], config=user_config):
pass
assert len(cb.traced_runs) == 1
run_metadata = cb.traced_runs[0].extra["metadata"]
assert "my-app" in run_metadata["versions"]
assert "langchain-fake" in run_metadata["versions"]
assert "langchain-core" in run_metadata["versions"]
def test_add_version_with_none_metadata() -> None:
"""Model constructed with metadata=None should still get versions."""
llm = FakeListChatModel(responses=["x"], metadata=None)
assert llm.metadata is not None
assert "langchain-core" in llm.metadata["versions"]
def test_add_version_with_non_dict_versions() -> None:
"""Non-dict `versions` value is silently replaced with a warning."""
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
llm = FakeListChatModel(responses=["x"], metadata={"versions": "garbage"})
assert any("expected a dict" in str(warning.message) for warning in w)
assert llm.metadata is not None
assert isinstance(llm.metadata["versions"], dict)
assert "langchain-core" in llm.metadata["versions"]
def test_langchain_version_in_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
"""When `langchain` is installed, its version appears in metadata."""
def _fake_version(pkg: str) -> str:
if pkg == "langchain":
return "1.2.13"
raise PackageNotFoundError(pkg)
monkeypatch.setattr("importlib.metadata.version", _fake_version)
_get_langchain_version.cache_clear()
try:
llm = FakeListChatModel(responses=["x"])
assert llm.metadata is not None
assert llm.metadata["versions"]["langchain"] == "1.2.13"
assert "langchain-core" in llm.metadata["versions"]
finally:
_get_langchain_version.cache_clear()
def test_langchain_version_missing_when_not_installed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When `langchain` is not installed, metadata.versions has no entry.
The lookup raises `PackageNotFoundError` (the real not-installed signal), which
`_get_langchain_version` must swallow without dropping the rest of the dict.
"""
def _raise_not_found(pkg: str) -> str:
raise PackageNotFoundError(pkg)
monkeypatch.setattr("importlib.metadata.version", _raise_not_found)
_get_langchain_version.cache_clear()
try:
llm = FakeListChatModel(responses=["x"])
assert llm.metadata is not None
assert "langchain" not in llm.metadata["versions"]
assert "langchain-core" in llm.metadata["versions"]
finally:
_get_langchain_version.cache_clear()
def test_version_validator_coexists_with_core_seed() -> None:
"""A `model_validator(mode="after")` calling `_add_version` keeps the core seed.
Mirrors the real partner pattern (a validator, not a `model_post_init`
override) and confirms the validator-added entry and the core-seeded
`langchain-core` entry accumulate rather than clobber one another.
"""
class _ValidatorVersionedModel(FakeListChatModel):
@model_validator(mode="after")
def _set_fake_version(self) -> Self:
self._add_version("langchain-fake", "0.1.0")
return self
llm = _ValidatorVersionedModel(responses=["x"])
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert versions["langchain-core"] == VERSION
assert versions["langchain-fake"] == "0.1.0"
def test_subclass_unique_validator_names_accumulate() -> None:
"""Parent and child uniquely-named validators must both contribute entries.
Regression guard for the documented Pydantic footgun: same-named
`model_validator` methods *replace* rather than chain across a class
hierarchy. As long as each layer uses a unique validator name, a subclass
must not silently drop its parent's version entry.
"""
class _ParentModel(FakeListChatModel):
@model_validator(mode="after")
def _set_parent_version(self) -> Self:
self._add_version("langchain-parent", "1.0.0")
return self
class _ChildModel(_ParentModel):
@model_validator(mode="after")
def _set_child_version(self) -> Self:
self._add_version("langchain-child", "2.0.0")
return self
llm = _ChildModel(responses=["x"])
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert versions["langchain-core"] == VERSION
assert versions["langchain-parent"] == "1.0.0"
assert versions["langchain-child"] == "2.0.0"
def test_model_profiles() -> None:
model = GenericFakeChatModel(messages=iter([]))
assert model.profile is None
@@ -1592,6 +1777,7 @@ def test_invocation_params_passed_to_tracer_metadata() -> None:
"ls_temperature": 0.7,
"stop": None,
"temperature": 0.7,
"versions": {"langchain-core": VERSION},
},
"options": {"stop": None},
"runtime": run.extra["runtime"],
@@ -84,7 +84,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['foo'], i=1)",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'], i=1)",
"name": "FakeListLLM"
}
},
@@ -128,7 +128,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['bar'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bar'])",
"name": "FakeListLLM"
}
},
@@ -268,7 +268,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['foo'], i=1)",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'], i=1)",
"name": "FakeListLLM"
},
"fallbacks": [
@@ -281,7 +281,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['bar'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bar'])",
"name": "FakeListLLM"
}
],
@@ -322,7 +322,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['foo'], i=1)",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'], i=1)",
"name": "FakeListLLM"
},
"fallbacks": [
@@ -335,7 +335,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['baz'], i=1)",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['baz'], i=1)",
"name": "FakeListLLM"
},
{
@@ -347,7 +347,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['bar'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bar'])",
"name": "FakeListLLM"
}
],
@@ -97,7 +97,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['foo, bar'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
"name": "FakeListChatModel"
}
],
@@ -227,7 +227,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['baz, qux'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['baz, qux'])",
"name": "FakeListChatModel"
}
],
@@ -346,7 +346,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['foo, bar'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
"name": "FakeListChatModel"
},
{
@@ -457,7 +457,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['baz, qux'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['baz, qux'])",
"name": "FakeListChatModel"
}
],
@@ -848,7 +848,7 @@
"fake",
"FakeStreamingListLLM"
],
"repr": "FakeStreamingListLLM(responses=['first item, second item, third item'])",
"repr": "FakeStreamingListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['first item, second item, third item'])",
"name": "FakeStreamingListLLM"
},
{
@@ -884,7 +884,7 @@
"fake",
"FakeStreamingListLLM"
],
"repr": "FakeStreamingListLLM(responses=['this', 'is', 'a', 'test'])",
"repr": "FakeStreamingListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['this', 'is', 'a', 'test'])",
"name": "FakeStreamingListLLM"
}
},
@@ -1009,7 +1009,7 @@
# name: test_prompt_with_chat_model
'''
ChatPromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, template='{question}'), additional_kwargs={})])
| FakeListChatModel(responses=['foo'])
| FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])
'''
# ---
# name: test_prompt_with_chat_model.1
@@ -1109,7 +1109,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['foo'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])",
"name": "FakeListChatModel"
}
},
@@ -1220,7 +1220,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['foo, bar'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
"name": "FakeListChatModel"
}
],
@@ -1249,7 +1249,7 @@
# name: test_prompt_with_chat_model_async
'''
ChatPromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, template='{question}'), additional_kwargs={})])
| FakeListChatModel(responses=['foo'])
| FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])
'''
# ---
# name: test_prompt_with_chat_model_async.1
@@ -1349,7 +1349,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['foo'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])",
"name": "FakeListChatModel"
}
},
@@ -1459,7 +1459,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['foo', 'bar'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo', 'bar'])",
"name": "FakeListLLM"
}
},
@@ -1576,7 +1576,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['foo', 'bar'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo', 'bar'])",
"name": "FakeListLLM"
}
],
@@ -1699,7 +1699,7 @@
"fake",
"FakeStreamingListLLM"
],
"repr": "FakeStreamingListLLM(responses=['bear, dog, cat', 'tomato, lettuce, onion'])",
"repr": "FakeStreamingListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bear, dog, cat', 'tomato, lettuce, onion'])",
"name": "FakeStreamingListLLM"
}
],
@@ -1867,7 +1867,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['4'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['4'])",
"name": "FakeListLLM"
}
},
@@ -1940,7 +1940,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=['2'])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['2'])",
"name": "FakeListLLM"
}
},
@@ -13407,7 +13407,7 @@
just_to_test_lambda: RunnableLambda(...)
}
| ChatPromptTemplate(input_variables=['documents', 'question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['documents', 'question'], input_types={}, partial_variables={}, template='Context:\n{documents}\n\nQuestion:\n{question}'), additional_kwargs={})])
| FakeListChatModel(responses=['foo, bar'])
| FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])
| CommaSeparatedListOutputParser()
'''
# ---
@@ -13610,7 +13610,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=['foo, bar'])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
"name": "FakeListChatModel"
}
],
@@ -13636,8 +13636,8 @@
ChatPromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, template='{question}'), additional_kwargs={})])
| RunnableLambda(...)
| {
chat: FakeListChatModel(responses=["i'm a chatbot"]),
llm: FakeListLLM(responses=["i'm a textbot"])
chat: FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=["i'm a chatbot"]),
llm: FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=["i'm a textbot"])
}
'''
# ---
@@ -13762,7 +13762,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=[\"i'm a chatbot\"])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a chatbot\"])",
"name": "FakeListChatModel"
},
"llm": {
@@ -13774,7 +13774,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=[\"i'm a textbot\"])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a textbot\"])",
"name": "FakeListLLM"
}
}
@@ -13917,7 +13917,7 @@
"fake_chat_models",
"FakeListChatModel"
],
"repr": "FakeListChatModel(responses=[\"i'm a chatbot\"])",
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a chatbot\"])",
"name": "FakeListChatModel"
},
"kwargs": {
@@ -13938,7 +13938,7 @@
"fake",
"FakeListLLM"
],
"repr": "FakeListLLM(responses=[\"i'm a textbot\"])",
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a textbot\"])",
"name": "FakeListLLM"
},
"passthrough": {
@@ -17,6 +17,7 @@ from langchain_core.runnables import RunnableBinding, RunnablePassthrough
from langchain_core.runnables.config import (
RunnableConfig,
_get_langsmith_inheritable_metadata_from_config,
_merge_metadata_dicts,
_set_config_context,
ensure_config,
merge_configs,
@@ -322,3 +323,90 @@ async def test_run_in_executor() -> None:
with pytest.raises(RuntimeError):
await run_in_executor(None, raises_stop_iter)
class TestMergeMetadataDicts:
"""Tests for _merge_metadata_dicts deep-merge behavior."""
def test_deep_merge_preserves_both_nested_dicts(self) -> None:
base = {"versions": {"langchain-core": "0.3.1"}, "user_id": "abc"}
incoming = {"versions": {"langchain-anthropic": "1.3.3"}, "run": "x"}
result = _merge_metadata_dicts(base, incoming)
assert result == {
"versions": {
"langchain-core": "0.3.1",
"langchain-anthropic": "1.3.3",
},
"user_id": "abc",
"run": "x",
}
def test_last_writer_wins_within_nested_dicts(self) -> None:
base = {"versions": {"pkg": "1.0"}}
incoming = {"versions": {"pkg": "2.0"}}
result = _merge_metadata_dicts(base, incoming)
assert result == {"versions": {"pkg": "2.0"}}
def test_non_dict_overwrites_dict(self) -> None:
base = {"key": {"nested": "value"}}
incoming = {"key": "flat"}
result = _merge_metadata_dicts(base, incoming)
assert result == {"key": "flat"}
def test_dict_overwrites_non_dict(self) -> None:
base = {"key": "flat"}
incoming = {"key": {"nested": "value"}}
result = _merge_metadata_dicts(base, incoming)
assert result == {"key": {"nested": "value"}}
def test_no_mutation_of_inputs(self) -> None:
base = {"versions": {"a": "1"}}
incoming = {"versions": {"b": "2"}}
base_copy = {"versions": {"a": "1"}}
incoming_copy = {"versions": {"b": "2"}}
result = _merge_metadata_dicts(base, incoming)
assert base == base_copy
assert incoming == incoming_copy
# Returned nested dicts should not share identity with originals.
assert result["versions"] is not base["versions"]
assert result["versions"] is not incoming["versions"]
def test_non_overlapping_nested_dict_is_copied(self) -> None:
base = {"versions": {"a": "1"}, "extras": {"x": "y"}}
incoming = {"versions": {"b": "2"}}
result = _merge_metadata_dicts(base, incoming)
# "extras" was not in incoming — result should still be a copy.
assert result["extras"] is not base["extras"]
assert result["extras"] == {"x": "y"}
def test_both_empty(self) -> None:
assert _merge_metadata_dicts({}, {}) == {}
def test_empty_base(self) -> None:
incoming = {"versions": {"pkg": "1.0"}}
result = _merge_metadata_dicts({}, incoming)
assert result == {"versions": {"pkg": "1.0"}}
assert result["versions"] is not incoming["versions"]
result["versions"]["new"] = "2.0"
assert incoming == {"versions": {"pkg": "1.0"}}
def test_empty_incoming(self) -> None:
result = _merge_metadata_dicts({"versions": {"pkg": "1.0"}}, {})
assert result == {"versions": {"pkg": "1.0"}}
def test_merge_configs_with_none_metadata(self) -> None:
merged = merge_configs(
cast("RunnableConfig", {"metadata": None}),
{"metadata": {"versions": {"a": "1"}}},
)
assert merged["metadata"] == {"versions": {"a": "1"}}
def test_three_config_merge_accumulates(self) -> None:
c1: RunnableConfig = {"metadata": {"versions": {"a": "1"}}}
c2: RunnableConfig = {"metadata": {"versions": {"b": "2"}}}
c3: RunnableConfig = {"metadata": {"versions": {"c": "3"}}}
merged = merge_configs(c1, c2, c3)
assert merged["metadata"] == {
"versions": {"a": "1", "b": "2", "c": "3"},
}
@@ -91,6 +91,7 @@ from langchain_core.tracers import (
from langchain_core.tracers._compat import pydantic_copy
from langchain_core.tracers.context import collect_runs
from langchain_core.utils.pydantic import PYDANTIC_VERSION
from langchain_core.version import VERSION
from tests.unit_tests.pydantic_utils import _normalize_schema, _schema
from tests.unit_tests.stubs import AnyStr, _any_id_ai_message, _any_id_ai_message_chunk
@@ -2147,7 +2148,11 @@ async def test_prompt_with_llm(
"value": {
"end_time": None,
"final_output": None,
"metadata": {"ls_model_type": "llm", "ls_provider": "fakelist"},
"metadata": {
"ls_model_type": "llm",
"ls_provider": "fakelist",
"versions": {"langchain-core": VERSION},
},
"name": "FakeListLLM",
"start_time": "2023-01-01T00:00:00.000+00:00",
"streamed_output": [],
@@ -2361,6 +2366,7 @@ async def test_prompt_with_llm_parser(
"metadata": {
"ls_model_type": "llm",
"ls_provider": "fakestreaminglist",
"versions": {"langchain-core": VERSION},
},
"name": "FakeStreamingListLLM",
"start_time": "2023-01-01T00:00:00.000+00:00",
@@ -56,7 +56,7 @@ from langchain_core.utils.function_calling import (
from langchain_core.utils.pydantic import is_basemodel_subclass
from langchain_core.utils.utils import _build_model_kwargs
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import NotRequired, TypedDict
from typing_extensions import NotRequired, Self, TypedDict
from langchain_anthropic import __version__
from langchain_anthropic._client_utils import (
@@ -1168,6 +1168,12 @@ class ChatAnthropic(BaseChatModel):
all_required_field_names = get_pydantic_field_names(cls)
return _build_model_kwargs(values, all_required_field_names)
@model_validator(mode="after")
def _set_anthropic_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-anthropic", __version__)
return self
def _resolve_model_profile(self) -> ModelProfile | None:
profile = _get_default_model_profile(self.model) or None
if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:
@@ -2,7 +2,7 @@
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_anthropic/_version.py`. Intended for use as a
pre-commit hook to prevent version mismatches.
CI check to prevent version mismatches.
"""
import re
@@ -23,6 +23,7 @@ from pydantic import BaseModel, Field, SecretStr, ValidationError
from pytest import CaptureFixture, MonkeyPatch
from langchain_anthropic import ChatAnthropic
from langchain_anthropic._version import __version__
from langchain_anthropic.chat_models import (
_TOOL_CALL_ID_PATTERN,
_create_usage_metadata,
@@ -2131,6 +2132,8 @@ def test_anthropic_model_params() -> None:
"ls_max_tokens": 64000,
"ls_temperature": None,
}
assert llm.metadata is not None
assert llm.metadata["versions"]["langchain-anthropic"] == __version__
ls_params = llm._get_ls_params(model=MODEL_NAME)
assert ls_params.get("ls_model_name") == MODEL_NAME
+4
View File
@@ -55,6 +55,9 @@ format format_diff:
check_imports: $(shell find langchain_deepseek -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -62,6 +65,7 @@ check_imports: $(shell find langchain_deepseek -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,16 +1,8 @@
"""LangChain DeepSeek integration."""
from importlib import metadata
from langchain_deepseek._version import __version__
from langchain_deepseek.chat_models import ChatDeepSeek
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = [
"ChatDeepSeek",
"__version__",
@@ -0,0 +1,3 @@
"""Version information for `langchain-deepseek`."""
__version__ = "1.1.0"
@@ -27,6 +27,7 @@ from langchain_openai.chat_models.base import BaseChatOpenAI
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_deepseek._version import __version__
from langchain_deepseek.data._profiles import _PROFILES
DEFAULT_API_BASE = "https://api.deepseek.com/v1"
@@ -224,6 +225,16 @@ class ChatDeepSeek(BaseChatOpenAI):
ls_params["ls_provider"] = "deepseek"
return ls_params
@model_validator(mode="after")
def _set_deepseek_version(self) -> Self:
"""Set package version in metadata.
Named uniquely to avoid shadowing `BaseChatOpenAI._set_openai_chat_version`;
Pydantic replaces same-named validators rather than chaining them.
"""
self._add_version("langchain-deepseek", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate necessary environment vars and client params."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_deepseek/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_deepseek" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_deepseek/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_deepseek/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -437,3 +437,13 @@ def test_profile() -> None:
model = ChatDeepSeek(model="deepseek-reasoner", api_key=SecretStr("test_key"))
assert model.profile is not None
assert model.profile["reasoning_output"]
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = ChatDeepSeek(model=MODEL_NAME, api_key=SecretStr("test_key"))
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-deepseek" in versions
assert "langchain-openai" in versions
@@ -0,0 +1,10 @@
"""Test `langchain_deepseek` public API surface."""
from langchain_deepseek import __all__
EXPECTED_ALL = ["__version__", "ChatDeepSeek"]
def test_all_imports() -> None:
"""Verify that `__all__` exports match the expected public API."""
assert sorted(EXPECTED_ALL) == sorted(__all__)
+4
View File
@@ -53,6 +53,9 @@ format format_diff:
check_imports: $(shell find langchain_fireworks -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -60,6 +63,7 @@ check_imports: $(shell find langchain_fireworks -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,9 +1,9 @@
"""Fireworks AI integration for LangChain."""
from langchain_fireworks._version import __version__
from langchain_fireworks.chat_models import ChatFireworks
from langchain_fireworks.embeddings import FireworksEmbeddings
from langchain_fireworks.llms import Fireworks
from langchain_fireworks.version import __version__
__all__ = [
"ChatFireworks",
@@ -0,0 +1,3 @@
"""Version information for `langchain-fireworks`."""
__version__ = "1.4.2"
@@ -100,6 +100,7 @@ from pydantic import (
from typing_extensions import Self
from langchain_fireworks._compat import _convert_from_v1_to_chat_completions
from langchain_fireworks._version import __version__
from langchain_fireworks.data._profiles import _PROFILES
logger = logging.getLogger(__name__)
@@ -827,6 +828,12 @@ class ChatFireworks(BaseChatModel):
all_required_field_names = get_pydantic_field_names(cls)
return _build_model_kwargs(values, all_required_field_names)
@model_validator(mode="after")
def _set_fireworks_chat_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-fireworks", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate that api key and python package exists in environment."""
@@ -15,8 +15,9 @@ from langchain_core.language_models.llms import LLM
from langchain_core.utils import get_pydantic_field_names
from langchain_core.utils.utils import _build_model_kwargs, secret_from_env
from pydantic import ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_fireworks.version import __version__
from langchain_fireworks._version import __version__
logger = logging.getLogger(__name__)
@@ -98,6 +99,12 @@ class Fireworks(LLM):
all_required_field_names = get_pydantic_field_names(cls)
return _build_model_kwargs(values, all_required_field_names)
@model_validator(mode="after")
def _set_fireworks_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-fireworks", __version__)
return self
@property
def _llm_type(self) -> str:
"""Return type of model."""
@@ -1,9 +1,3 @@
"""Main entrypoint into package."""
"""Backwards-compatible re-export from `langchain_fireworks._version`."""
from importlib import metadata
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
from langchain_fireworks._version import __version__ # noqa: F401
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_fireworks/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_fireworks" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_fireworks/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_fireworks/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -2,6 +2,7 @@
from __future__ import annotations
import os
from typing import Any
from unittest.mock import MagicMock
@@ -116,6 +117,16 @@ def test_convert_dict_to_message_without_reasoning_content() -> None:
assert "reasoning_content" not in message.additional_kwargs
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
os.environ.setdefault("FIREWORKS_API_KEY", "fake-key")
llm = ChatFireworks(model="accounts/fireworks/models/llama-v3-70b-instruct")
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-fireworks" in versions
def test_format_message_content_passthrough_string() -> None:
"""Plain string content is returned unchanged."""
assert _format_message_content("hello") == "hello"
+4
View File
@@ -54,6 +54,9 @@ format format_diff:
check_imports: $(shell find langchain_groq -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -61,6 +64,7 @@ check_imports: $(shell find langchain_groq -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,6 +1,9 @@
"""Groq integration for LangChain."""
from langchain_groq._version import __version__
from langchain_groq.chat_models import ChatGroq
from langchain_groq.version import __version__
__all__ = ["ChatGroq", "__version__"]
__all__ = [
"ChatGroq",
"__version__",
]
@@ -0,0 +1,3 @@
"""Version information for `langchain-groq`."""
__version__ = "1.1.3"
@@ -71,8 +71,8 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_groq._compat import _convert_from_v1_to_groq
from langchain_groq._version import __version__
from langchain_groq.data._profiles import _PROFILES
from langchain_groq.version import __version__
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
_STRICT_STRUCTURED_OUTPUT_MODELS = frozenset(
@@ -543,6 +543,12 @@ class ChatGroq(BaseChatModel):
raise ImportError(msg) from exc
return self
@model_validator(mode="after")
def _set_groq_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-groq", __version__)
return self
def _resolve_model_profile(self) -> ModelProfile | None:
return _get_default_model_profile(self.model_name) or None
+2 -8
View File
@@ -1,9 +1,3 @@
"""Main entrypoint into package."""
"""Backwards-compatible re-export from `langchain_groq._version`."""
from importlib import metadata
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
from langchain_groq._version import __version__ # noqa: F401
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_groq/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_groq" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_groq/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_groq/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1095,3 +1095,12 @@ def test_format_message_content_mixed() -> None:
{"type": "image_url", "image_url": {"url": "data:image/png;base64,<data>"}},
]
assert expected == _format_message_content(content)
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = ChatGroq(model="foo") # type: ignore[call-arg]
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-groq" in versions
+6 -2
View File
@@ -1,4 +1,4 @@
.PHONY: all format lint type test tests integration_tests help extended_tests
.PHONY: all format lint type test tests integration_tests help extended_tests check_version
# Default target executed when no arguments are given to make.
all: help
@@ -55,13 +55,17 @@ format format_diff:
check_imports: $(shell find langchain_huggingface -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,5 +1,6 @@
"""Hugging Face integration for LangChain."""
from langchain_huggingface._version import __version__
from langchain_huggingface.chat_models import (
ChatHuggingFace, # type: ignore[import-not-found]
)
@@ -18,4 +19,5 @@ __all__ = [
"HuggingFaceEndpoint",
"HuggingFaceEndpointEmbeddings",
"HuggingFacePipeline",
"__version__",
]
@@ -0,0 +1,3 @@
"""Version information for `langchain-huggingface`."""
__version__ = "1.2.2"
@@ -69,6 +69,7 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
from pydantic import BaseModel, Field, model_validator
from typing_extensions import Self
from langchain_huggingface._version import __version__
from langchain_huggingface.data._profiles import _PROFILES
from langchain_huggingface.llms.huggingface_endpoint import HuggingFaceEndpoint
from langchain_huggingface.llms.huggingface_pipeline import HuggingFacePipeline
@@ -580,6 +581,12 @@ class ChatHuggingFace(BaseChatModel):
):
self.model_kwargs = self.llm.model_kwargs.copy()
@model_validator(mode="after")
def _set_huggingface_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-huggingface", __version__)
return self
@model_validator(mode="after")
def validate_llm(self) -> Self:
if (
@@ -17,6 +17,8 @@ from langchain_core.utils import from_env, get_pydantic_field_names
from pydantic import ConfigDict, Field, model_validator
from typing_extensions import Self
from langchain_huggingface._version import __version__
logger = logging.getLogger(__name__)
@@ -249,6 +251,12 @@ class HuggingFaceEndpoint(LLM):
raise ValueError(msg)
return values
@model_validator(mode="after")
def _set_huggingface_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-huggingface", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate that package is installed and that the API token is valid."""
@@ -9,7 +9,9 @@ from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.language_models.llms import BaseLLM
from langchain_core.outputs import Generation, GenerationChunk, LLMResult
from pydantic import ConfigDict, model_validator
from typing_extensions import Self
from langchain_huggingface._version import __version__
from langchain_huggingface.utils.import_utils import (
IMPORT_ERROR,
is_ipex_available,
@@ -91,6 +93,12 @@ class HuggingFacePipeline(BaseLLM):
extra="forbid",
)
@model_validator(mode="after")
def _set_huggingface_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-huggingface", __version__)
return self
@model_validator(mode="before")
@classmethod
def pre_init_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_huggingface/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_huggingface" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_huggingface/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_huggingface/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -329,6 +329,17 @@ def test_inheritance_with_empty_llm() -> None:
assert chat.temperature is None
def test_metadata_versions(chat_hugging_face: Any) -> None:
"""Test that metadata reports the correct version info."""
from langchain_huggingface._version import __version__
assert chat_hugging_face.metadata is not None
versions = chat_hugging_face.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-huggingface" in versions
assert versions["langchain-huggingface"] == __version__
def test_profile() -> None:
empty_llm = Mock(spec=HuggingFaceEndpoint)
empty_llm.repo_id = "test/model"
+4
View File
@@ -56,6 +56,9 @@ format format_diff:
check_imports: $(shell find langchain_mistralai -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -63,6 +66,7 @@ check_imports: $(shell find langchain_mistralai -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,6 +1,11 @@
"""Mistral AI integration for LangChain."""
from langchain_mistralai._version import __version__
from langchain_mistralai.chat_models import ChatMistralAI
from langchain_mistralai.embeddings import MistralAIEmbeddings
__all__ = ["ChatMistralAI", "MistralAIEmbeddings"]
__all__ = [
"ChatMistralAI",
"MistralAIEmbeddings",
"__version__",
]
@@ -0,0 +1,3 @@
"""Version information for `langchain-mistralai`."""
__version__ = "1.1.5"
@@ -78,6 +78,7 @@ from pydantic import (
from typing_extensions import Self
from langchain_mistralai._compat import _convert_from_v1_to_mistral
from langchain_mistralai._version import __version__
from langchain_mistralai.data._profiles import _PROFILES
if TYPE_CHECKING:
@@ -661,6 +662,12 @@ class ChatMistralAI(BaseChatModel):
overall_token_usage[k] = v
return {"token_usage": overall_token_usage, "model_name": self.model}
@model_validator(mode="after")
def _set_mistralai_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-mistralai", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate api key, python package exists, temperature, and top_p."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_mistralai/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_mistralai" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_mistralai/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_mistralai/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -653,3 +653,12 @@ def test_no_duplicate_tool_calls_when_multiple_tools() -> None:
def test_profile() -> None:
model = ChatMistralAI(model="mistral-large-latest") # type: ignore[call-arg]
assert model.profile
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = ChatMistralAI(model="foo") # type: ignore[call-arg]
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-mistralai" in versions
@@ -1,6 +1,6 @@
from langchain_mistralai import __all__
EXPECTED_ALL = ["ChatMistralAI", "MistralAIEmbeddings"]
EXPECTED_ALL = ["__version__", "ChatMistralAI", "MistralAIEmbeddings"]
def test_all_imports() -> None:
+4
View File
@@ -60,6 +60,9 @@ format format_diff:
check_imports: $(shell find langchain_ollama -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -67,6 +70,7 @@ check_imports: $(shell find langchain_ollama -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -2,39 +2,13 @@
Provides infrastructure for interacting with the [Ollama](https://ollama.com/)
service.
!!! note
**Newly added in 0.3.4:** `validate_model_on_init` param on all models.
This parameter allows you to validate the model exists in Ollama locally on
initialization. If set to `True`, it will raise an error if the model does not
exist locally. This is useful for ensuring that the model is available before
attempting to use it, especially in environments where models may not be
pre-downloaded.
"""
from importlib import metadata
from importlib.metadata import PackageNotFoundError
from typing import NoReturn
from langchain_ollama._version import __version__
from langchain_ollama.chat_models import ChatOllama
from langchain_ollama.embeddings import OllamaEmbeddings
from langchain_ollama.llms import OllamaLLM
def _raise_package_not_found_error() -> NoReturn:
raise PackageNotFoundError
try:
if __package__ is None:
_raise_package_not_found_error()
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = [
"ChatOllama",
"OllamaEmbeddings",
@@ -0,0 +1,3 @@
"""Version information for `langchain-ollama`."""
__version__ = "1.1.0"
@@ -95,6 +95,7 @@ from langchain_ollama._utils import (
parse_url_with_auth,
validate_model,
)
from langchain_ollama._version import __version__
log = logging.getLogger(__name__)
@@ -921,6 +922,12 @@ class ChatOllama(BaseChatModel):
)
return schema
@model_validator(mode="after")
def _set_ollama_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-ollama", __version__)
return self
@model_validator(mode="after")
def _set_clients(self) -> Self:
"""Set clients to use for ollama."""
@@ -20,6 +20,7 @@ from langchain_ollama._utils import (
parse_url_with_auth,
validate_model,
)
from langchain_ollama._version import __version__
class OllamaLLM(BaseLLM):
@@ -319,6 +320,12 @@ class OllamaLLM(BaseLLM):
params["ls_max_tokens"] = max_tokens
return params
@model_validator(mode="after")
def _set_ollama_llm_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-ollama", __version__)
return self
@model_validator(mode="after")
def _set_clients(self) -> Self:
"""Set clients to use for ollama."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_ollama/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_ollama" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_ollama/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_ollama/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -814,6 +814,15 @@ def test_chat_ollama_ignores_strict_arg() -> None:
assert "strict" not in call_kwargs
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = ChatOllama(model=MODEL_NAME)
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-ollama" in versions
def test_chat_ollama_supports_response_format_json_schema() -> None:
"""Test that ChatOllama correctly maps json_schema response_format to format."""
with patch("langchain_ollama.chat_models.Client") as mock_client_class:
@@ -90,3 +90,12 @@ async def test_acreate_generate_stream_raises_when_client_none() -> None:
with pytest.raises(RuntimeError, match="async client is not initialized"):
async for _ in llm._acreate_generate_stream("Hello"):
pass
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = OllamaLLM(model=MODEL_NAME)
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-ollama" in versions
+4
View File
@@ -72,6 +72,9 @@ format format_diff:
check_imports: $(shell find langchain_openai -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -79,6 +82,7 @@ check_imports: $(shell find langchain_openai -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,5 +1,6 @@
"""Module for OpenAI integrations."""
from langchain_openai._version import __version__
from langchain_openai.chat_models import AzureChatOpenAI, ChatOpenAI
from langchain_openai.chat_models._client_utils import StreamChunkTimeoutError
from langchain_openai.embeddings import AzureOpenAIEmbeddings, OpenAIEmbeddings
@@ -14,5 +15,6 @@ __all__ = [
"OpenAI",
"OpenAIEmbeddings",
"StreamChunkTimeoutError",
"__version__",
"custom_tool",
]
@@ -0,0 +1,3 @@
"""Version information for `langchain-openai`."""
__version__ = "1.3.0"
@@ -130,6 +130,7 @@ from pydantic import (
from pydantic.v1 import BaseModel as BaseModelV1
from typing_extensions import Self
from langchain_openai._version import __version__
from langchain_openai.chat_models._client_utils import (
_astream_with_chunk_timeout,
_build_proxied_async_httpx_client,
@@ -1136,6 +1137,20 @@ class BaseChatOpenAI(BaseChatModel):
return values
@model_validator(mode="after")
def _set_openai_chat_version(self) -> Self:
"""Set package version in metadata.
Note: Subclasses that inherit from `BaseChatOpenAI` (e.g.
`ChatDeepSeek`, `ChatXAI`) must use a **unique** validator name
(e.g. `_set_deepseek_version`) instead of overriding this one. Pydantic
replaces same-named `model_validator` methods rather than chaining them,
so reusing `_set_openai_chat_version` would silently drop the parent's
`langchain-openai` version entry.
"""
self._add_version("langchain-openai", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate that api key and python package exists in environment."""
@@ -12,6 +12,7 @@ from langchain_core.utils import from_env, secret_from_env
from pydantic import Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_openai._version import __version__
from langchain_openai.llms.base import BaseOpenAI
logger = logging.getLogger(__name__)
@@ -117,6 +118,12 @@ class AzureOpenAI(BaseOpenAI):
"""Return whether this model can be serialized by LangChain."""
return True
@model_validator(mode="after")
def _set_azure_openai_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-openai", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate that api key and python package exists in environment."""
@@ -21,6 +21,7 @@ from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_fro
from pydantic import ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_openai._version import __version__
from langchain_openai.data._profiles import _PROFILES
logger = logging.getLogger(__name__)
@@ -310,6 +311,12 @@ class BaseOpenAI(BaseLLM):
all_required_field_names = get_pydantic_field_names(cls)
return _build_model_kwargs(values, all_required_field_names)
@model_validator(mode="after")
def _set_openai_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-openai", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate that api key and python package exists in environment."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_openai/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_openai" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_openai/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_openai/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -269,3 +269,16 @@ def test_max_tokens_converted_to_max_completion_tokens() -> None:
assert "max_completion_tokens" in payload
assert payload["max_completion_tokens"] == 1000
assert "max_tokens" not in payload
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = AzureChatOpenAI( # type: ignore[call-arg]
azure_deployment="35-turbo-dev",
openai_api_version="2023-05-15",
azure_endpoint="my-base-url",
)
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-openai" in versions
@@ -4077,6 +4077,15 @@ def test_context_overflow_error_backwards_compatibility() -> None:
assert isinstance(exc_info.value, ContextOverflowError)
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = ChatOpenAI()
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-openai" in versions
def test_get_request_payload_responses_api_input_file_blocks_passthrough() -> None:
llm = ChatOpenAI(model="gpt-5", use_responses_api=True)
payload = llm._get_request_payload(
@@ -0,0 +1,62 @@
"""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
@@ -1,6 +1,7 @@
from langchain_openai import __all__
EXPECTED_ALL = [
"__version__",
"OpenAI",
"ChatOpenAI",
"OpenAIEmbeddings",
+4
View File
@@ -55,6 +55,9 @@ format format_diff:
check_imports: $(shell find langchain_openrouter -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -62,6 +65,7 @@ check_imports: $(shell find langchain_openrouter -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,7 +1,9 @@
"""LangChain OpenRouter integration."""
from langchain_openrouter._version import __version__
from langchain_openrouter.chat_models import ChatOpenRouter
__all__ = [
"ChatOpenRouter",
"__version__",
]
@@ -0,0 +1,3 @@
"""Version information for `langchain-openrouter`."""
__version__ = "0.2.3"
@@ -69,6 +69,7 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_openrouter._version import __version__
from langchain_openrouter.data._profiles import _PROFILES
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
@@ -410,6 +411,12 @@ class ChatOpenRouter(BaseChatModel):
)
return openrouter.OpenRouter(**client_kwargs)
@model_validator(mode="after")
def _set_openrouter_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-openrouter", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate configuration and build the SDK client."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_openrouter/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_openrouter" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_openrouter/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_openrouter/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -274,6 +274,14 @@ class TestChatOpenRouterInstantiation:
ls_params = model._get_ls_params()
assert ls_params["ls_stop"] == ["END", "STOP"]
def test_metadata_versions(self) -> None:
"""Test that metadata reports the correct version info."""
model = _make_model()
assert model.metadata is not None
versions = model.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-openrouter" in versions
def test_client_created(self) -> None:
"""Test that OpenRouter SDK client is created."""
model = _make_model()
@@ -3,6 +3,7 @@
from langchain_openrouter import __all__
EXPECTED_ALL = [
"__version__",
"ChatOpenRouter",
]
+6 -2
View File
@@ -1,4 +1,4 @@
.PHONY: all format lint type test tests integration_tests help extended_tests
.PHONY: all format lint type test tests integration_tests help extended_tests check_version
# Default target executed when no arguments are given to make.
all: help
@@ -54,13 +54,17 @@ format format_diff:
check_imports: $(shell find langchain_perplexity -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
@@ -1,5 +1,6 @@
"""Perplexity AI integration for LangChain."""
from langchain_perplexity._version import __version__
from langchain_perplexity.chat_models import ChatPerplexity
from langchain_perplexity.embeddings import PerplexityEmbeddings
from langchain_perplexity.output_parsers import (
@@ -17,6 +18,7 @@ from langchain_perplexity.types import (
)
__all__ = [
"__version__",
"ChatPerplexity",
"PerplexityEmbeddings",
"PerplexitySearchRetriever",
@@ -0,0 +1,3 @@
"""Version information for `langchain-perplexity`."""
__version__ = "1.4.0"
@@ -56,6 +56,7 @@ from perplexity import AsyncPerplexity, Perplexity
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_perplexity._version import __version__
from langchain_perplexity.data._profiles import _PROFILES
from langchain_perplexity.output_parsers import (
ReasoningJsonOutputParser,
@@ -839,6 +840,12 @@ class ChatPerplexity(BaseChatModel):
values["model_kwargs"] = extra
return values
@model_validator(mode="after")
def _set_perplexity_version(self) -> Self:
"""Set package version in metadata."""
self._add_version("langchain-perplexity", __version__)
return self
@model_validator(mode="after")
def validate_environment(self) -> Self:
"""Validate that api key and python package exists in environment."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_perplexity/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_perplexity" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_perplexity/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_perplexity/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -223,6 +223,18 @@ def test_perplexity_invoke_includes_num_search_queries(mocker: MockerFixture) ->
patcher.assert_called_once()
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
from langchain_perplexity._version import __version__
llm = ChatPerplexity(model="test")
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-perplexity" in versions
assert versions["langchain-perplexity"] == __version__
def test_profile() -> None:
model = ChatPerplexity(model="sonar")
assert model.profile
@@ -1,6 +1,7 @@
from langchain_perplexity import __all__
EXPECTED_ALL = [
"__version__",
"ChatPerplexity",
"PerplexityEmbeddings",
"PerplexitySearchRetriever",
+4
View File
@@ -53,6 +53,9 @@ format format_diff:
check_imports: $(shell find langchain_xai -name '*.py')
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
######################
# HELP
######################
@@ -60,6 +63,7 @@ check_imports: $(shell find langchain_xai -name '*.py')
help:
@echo '----'
@echo 'check_imports - check imports'
@echo 'check_version - validate version consistency'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'type - run type checking'
+5 -1
View File
@@ -1,5 +1,9 @@
"""LangChain integration with xAI."""
from langchain_xai._version import __version__
from langchain_xai.chat_models import ChatXAI
__all__ = ["ChatXAI"]
__all__ = [
"ChatXAI",
"__version__",
]
@@ -0,0 +1,3 @@
"""Version information for `langchain-xai`."""
__version__ = "1.2.2"
@@ -12,6 +12,7 @@ from langchain_openai.chat_models.base import BaseChatOpenAI
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_xai._version import __version__
from langchain_xai.data._profiles import _PROFILES
if TYPE_CHECKING:
@@ -468,6 +469,16 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override]
params["ls_provider"] = "xai"
return params
@model_validator(mode="after")
def _set_xai_version(self) -> Self:
"""Set package version in metadata.
Named uniquely to avoid shadowing `BaseChatOpenAI._set_openai_chat_version`;
Pydantic replaces same-named validators rather than chaining them.
"""
self._add_version("langchain-xai", __version__)
return self
@model_validator(mode="after")
def _warn_search_parameters_deprecated(self) -> Self:
"""Emit deprecation warning if search_parameters (Live Search) is used."""
@@ -0,0 +1,65 @@
"""Check version consistency between `pyproject.toml` and `_version.py`.
This script validates that the version defined in pyproject.toml matches the
`__version__` variable in `langchain_xai/_version.py`. Intended for use as a
CI check to prevent version mismatches.
"""
import re
import sys
from pathlib import Path
def get_pyproject_version(pyproject_path: Path) -> str | None:
"""Extract version from `pyproject.toml`."""
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def get_version_py_version(version_path: Path) -> str | None:
"""Extract `__version__` from `_version.py`."""
content = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
return match.group(1) if match else None
def main() -> int:
"""Validate version consistency."""
script_dir = Path(__file__).parent
package_dir = script_dir.parent
pyproject_path = package_dir / "pyproject.toml"
version_path = package_dir / "langchain_xai" / "_version.py"
if not pyproject_path.exists():
print(f"Error: {pyproject_path} not found") # noqa: T201
return 1
if not version_path.exists():
print(f"Error: {version_path} not found") # noqa: T201
return 1
pyproject_version = get_pyproject_version(pyproject_path)
version_py_version = get_version_py_version(version_path)
if pyproject_version is None:
print("Error: Could not find version in pyproject.toml") # noqa: T201
return 1
if version_py_version is None:
print("Error: Could not find __version__ in langchain_xai/_version.py") # noqa: T201
return 1
if pyproject_version != version_py_version:
print("Error: Version mismatch detected!") # noqa: T201
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
print(f" langchain_xai/_version.py: {version_py_version}") # noqa: T201
return 1
print(f"Version check passed: {pyproject_version}") # noqa: T201
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -161,3 +161,13 @@ def test_stream_usage_metadata() -> None:
model = ChatXAI(model=MODEL_NAME, stream_usage=False)
assert model.stream_usage is False
def test_metadata_versions() -> None:
"""Test that metadata reports the correct version info."""
llm = ChatXAI(model=MODEL_NAME)
assert llm.metadata is not None
versions = llm.metadata["versions"]
assert "langchain-core" in versions
assert "langchain-xai" in versions
assert "langchain-openai" in versions
@@ -1,6 +1,6 @@
from langchain_xai import __all__
EXPECTED_ALL = ["ChatXAI"]
EXPECTED_ALL = ["__version__", "ChatXAI"]
def test_all_imports() -> None: