feat(core,partners): add package version tracking to tracing metadata - #35295
Conversation
Merging this PR will degrade performance by 84.24%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | WallTime | test_import_time[HumanMessage] |
232.4 ms | 207.3 ms | +12.09% |
| ⚡ | WallTime | test_import_time[RunnableLambda] |
452.3 ms | 394.1 ms | +14.77% |
| ⚡ | WallTime | test_import_time[LangChainTracer] |
404.5 ms | 361.3 ms | +11.96% |
| ⚡ | WallTime | test_import_time[tool] |
485.6 ms | 422.3 ms | +14.99% |
| ⚡ | WallTime | test_async_callbacks_in_sync |
20.1 ms | 14.8 ms | +35.15% |
| ⚡ | WallTime | test_import_time[InMemoryVectorStore] |
547 ms | 496.2 ms | +10.23% |
| ⚡ | WallTime | test_import_time[PydanticOutputParser] |
495.7 ms | 440.5 ms | +12.54% |
| ⚡ | WallTime | test_import_time[ChatPromptTemplate] |
583.1 ms | 490.1 ms | +18.97% |
| ❌ | WallTime | test_init_time |
3.6 ms | 10.2 ms | -65.29% |
| ❌ | WallTime | test_init_time |
3.5 ms | 10.3 ms | -65.48% |
| ❌ | Simulation | test_init_time_with_client |
3 ms | 12.8 ms | -76.33% |
| ❌ | Simulation | test_init_time |
1.9 ms | 11.8 ms | -84.24% |
| ❌ | WallTime | test_init_time |
453.7 ms | 529.4 ms | -14.29% |
| ❌ | Simulation | test_init_time |
7.3 ms | 9.1 ms | -19.49% |
Comparing mdrxy/versioning (4d77e2b) with master (2f64d80)
Footnotes
-
14 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
…ions - Update test_invocation_params_passed_to_tracer_metadata and syrupy snapshots to account for metadata['versions'] now on traces - Sync stale partner _version.py files with pyproject.toml versions - Clarify _merge_metadata_dicts docstring (one level deep, not recursive) Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
) Stamp `metadata.versions` on traces to surface the `deepagents` and `deepagents-cli` versions that produced a run. Consolidates the duplicated stream config builder from `textual_adapter.py` and `non_interactive.py` into a single `build_stream_config` in `config.py`. Upstream `langchain-core` [#35295](langchain-ai/langchain#35295) is **not required** — this PR is self-contained. Once it lands, inner LLM call traces will additionally carry `langchain-core` / `langchain-anthropic` / etc. versions via `_add_version()`, and the `add_metadata` deep-merge in callbacks will preserve them alongside the graph-level versions set here. ## Changes - `build_stream_config` injects both `deepagents-cli` and `deepagents` SDK versions into `metadata["versions"]` — the CLI must include both because LangGraph shallow-merges metadata dicts, so the CLI's `versions` dict replaces the SDK's at stream time - SDK version is resolved via `importlib.metadata.version("deepagents")` (not a direct import) to avoid pulling the heavy SDK package into the CLI's startup path ## How version metadata flows through a trace `create_deep_agent` bakes `versions: {"deepagents": "X.Y.Z"}` into the compiled graph via `with_config`. The CLI adds `{"deepagents-cli": "X.Y.Z"}` via `build_stream_config`. LangGraph propagates this config metadata down to every node in the graph. With `langchain-core` [#35295](langchain-ai/langchain#35295), each chat model will also stamps its own versions (`langchain-core`, `langchain-anthropic`, etc.) on `self.metadata` via `_add_version()` in the constructor. The `add_metadata` deep-merge in callbacks ensures these survive alongside the inherited graph-level versions. The final inner LLM call trace carries all versions: ```json { "ls_integration": "deepagents", "versions": { "deepagents": "0.5.0", "deepagents-cli": "0.0.34", "langchain-core": "0.3.42", "langchain-anthropic": "1.3.3" } } ```
Following on the heels of #35293
TODO:
Summary
Surface partner package versions in
metadata.versionson LangSmith traces. Mirrors the JS SDK's_addVersion()pattern (langchainjs#10106).Each model constructor records its package version via
_add_version()onBaseLanguageModel. The version dict accumulates through the class hierarchy —langchain-coreis added inBaseLanguageModel.model_post_init,langchain-openaiinBaseChatOpenAI._set_openai_chat_version, and each leaf partner in its uniquely-namedmodel_validator. Traces end up with:{ "metadata": { "versions": { "langchain-core": "1.4.5", "langchain-openai": "1.3.0", "langchain-xai": "1.2.2" } } }Changes
BaseLanguageModel._add_version(pkg, version)— appends toself.metadata["versions"]; accepts anyMappingtype; emits a warning if a non-mapping value is found and replacedBaseLanguageModel.model_post_init— addslangchain-coreversion; callssuper()for MRO safety_merge_metadata_dicts— one-level-deep (non-recursive) merge for nested dict metadata keysCallbackManager.add_metadata— uses_merge_metadata_dictsinstead of flatdict.update()so nested metadata dicts (likeversions) coexist rather than clobbermerge_configs— uses_merge_metadata_dictsfor config mergingPartners:
self._add_version("langchain-<pkg>", __version__)Design decisions
_get_ls_params-based — versions flow throughself.metadata(local metadata on traces), not throughLangSmithParams. This matches JS and makes child-class version inheritance automatic (no merge/clobber issues).versionsis local (non-inheritable) metadata —self.metadatais passed toCallbackManager.configureaslocal_metadata(add_metadata(..., inherit=False)), soversionsis 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 chore(core): reduce streaming metadata / perf #36588 was reducing for performance —versionsdoes not regress that path.add_metadatadeep-merge is a correctness fix, not just for versions — previouslyadd_metadata/merge_configsdid a flat top-leveldict.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 passedmetadata._merge_metadata_dictsmerges one level deep so user-providedconfig.metadata.versionsand model-setversionscoexist instead of clobbering. The merge runs once perconfigure(not per chunk), so it is off the streaming hot path._merge_metadata_dictsis deliberately not a recursive deep merge; values nested more than one level are last-writer-wins. This covers theversionscase without the ambiguity/cost of arbitrary-depth merging.metadata["versions"]— if a user setsmetadata={"versions": "some-string"},_add_versionemits 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)
mergeConfigsstill flat-spreads nested metadata, sometadata.versionscan still clobber on the JS side until an equivalent deep-merge lands.Made by Open SWE