* feat(bundles): add lfx-bundles metapackage skeleton
Creates the manifest-less lfx-bundles distribution (the langchain-community
model) that the long-tail providers will move into. Empty skeleton for now;
the bulk move (scripts/migrate/consolidate_bundles.py) populates the provider
folders + per-provider extras later.
- src/bundles/lfx-bundles: a single pyproject declaring the lfx.bundles
entry-point (lfx_bundles = "lfx_bundles"), an lfx>=1.10.0,<2.0.0 pin, and a
generated (currently empty) `all` extra; plus a bare lfx_bundles namespace
package and a README documenting the model + install stories
- wired into the root workspace via the existing bundle marker blocks:
dep lfx-bundles[all]>=1.0,<2.0, uv source, and member
- hyphen dir name so release.yml's src/bundles/*/pyproject.toml glob builds
it with zero workflow change
Verified: the wheel builds (entry_points.txt carries the lfx.bundles group);
load_lfx_bundles_extensions (PR-1) discovers the entry point and the empty
skeleton registers zero providers with no error.
* feat(bundles): consolidate first long-tail tranche into lfx-bundles
Adds scripts/migrate/consolidate_bundles.py (the inverse of port_bundle.py --
moves in-tree providers into the manifest-less lfx-bundles metapackage) and
runs it on a verified 5-provider tranche: tavily, exa, wikipedia, yahoosearch,
wolframalpha.
Per provider the script:
- moves src/lfx/src/lfx/components/<p>/ -> lfx_bundles/<p>/ (lowercase names);
- leaves a fail-soft import shim (first line `# lfx-bundles-shim`) so
`from lfx.components.<p> import X` keeps working when lfx-bundles is
installed, and raises an actionable ImportError otherwise;
- merges the provider's third-party deps into a PEP 685-normalized lfx-bundles
extra and regenerates the `all` aggregate. Dep parity holds: `uv sync` is a
no-op because those deps were already pulled via langflow-base[complete];
- appends the 4-entry migration block per Component class (28 entries) so saved
flows referencing lfx.components.<p>.<Class> migrate to
ext:<p>:<Class>@official.
To avoid double registration, the in-tree component walk
(_load_components_dynamically) now skips shimmed provider dirs, and
component_index.json is regenerated (355->348 components, 95->90 modules); the
moved providers load only at @official via lfx.bundles discovery.
Verified: discovery finds all 5 at @official with no errors; shims resolve;
`import lfx.components` still works; the index drops the 7 moved component
entries (residual `tools`-category name refs resolve via the shim); ruff clean.
First tranche proves the engine; the remaining long-tail scales by extending
PROVIDER_DEPS (each provider's deps verified individually -- the careful part).
* feat(bundles): consolidate 30-provider tranche 2 into lfx-bundles
Extends PROVIDER_DEPS with 30 individually-verified providers and runs the
consolidation: vector stores (chroma, clickhouse, couchbase, milvus, mongodb,
pgvector, pinecone, qdrant, supabase, upstash, weaviate), model providers
(groq, mistral, ollama, perplexity, sambanova), and tools/memory/data (apify,
assemblyai, confluence, firecrawl, git, glean, icosacomputing, mem0, needle,
scrapegraph, serpapi, unstructured, youtube, zep).
Dep verification (the careful part): every spec comes from langflow-base's
per-provider extras or its direct dependencies; langchain-community providers
carry the wrapper plus the SDK the wrapper lazy-imports (e.g. pgvector,
atlassian-python-api); requests is declared explicitly where imported (it is
only transitive in today's env); pinecone keeps its python_version<'3.14'
marker verbatim. Tranche excludes: providers with langflow imports (vlmrun),
provider-specific lfx.base dirs (composio/huggingface/langwatch), case-
sensitive names (FAISS/Notion), the openai-SDK family (azure/aiml/deepseek/
litellm/lmstudio/novita/openrouter/vllm/xai/cometapi -- cleaner after PR-8),
and the partner set.
Dep parity verified at the resolution level: the uv.lock diff is +220 lines of
lfx-bundles extras metadata with ZERO packages added or removed (`name =` diff
empty), so pip install langflow resolves the identical set.
Also: 192 append-only migration entries (48 classes x 4, zero bare-name
ambiguities); component_index.json regenerated 348->300 components / 90->60
modules (exactly the moved set, no stale standalone entries); mongodb_atlas.py
SLF001 per-file-ignore and the mem0/mongodb detect-secrets baseline entries
migrated to the new paths (lint/secrets exceptions travel with moved files);
35 bundles now discover at @official with 55 components; shims verified across
categories; 449 extension tests pass.
* feat(bundles): consolidate openai-SDK family tranche 3 into lfx-bundles
10 providers that ride the langchain-openai wrapper, deferred from tranche 2
until the partner graduation settled the openai-SDK dep story: aiml, azure,
cometapi, deepseek, litellm, lmstudio, novita, openrouter, vllm, xai.
Dep verification: every provider declares langchain-openai>=1.1.6; the openai
SDK is declared only where a component imports it directly (aiml, deepseek,
litellm, lmstudio, vllm, xai -- wrapper-transitive elsewhere); requests
declared where imported (cometapi, deepseek, novita, xai); lmstudio's lazy
NVIDIAEmbeddings path gets langchain-nvidia-ai-endpoints~=1.0.0. The litellm
component drives LiteLLM-served endpoints through the OpenAI client and does
NOT import the litellm package -- langflow-base's litellm extra stays put.
These providers use the lazy _dynamic_imports __init__ shape; it survives the
move unchanged (import_mod resolves via __spec__.parent and
lfx.components._importing remains a core helper).
Dep parity: uv.lock diff has zero package additions/removals (all specs
already resolved via langflow-base[complete]).
Also: 56 append-only migration entries (14 classes x 4, zero ambiguities);
component_index.json regenerated 300->286 components / 60->50 modules (exactly
the moved set); detect-secrets baseline re-keyed; 45 bundles now discover at
@official with 69 components; shims verified (azure/xai/litellm/deepseek);
ruff clean.
* fix(ci): nightly bundle rename follows [extras] refs and self-refs
Two gaps in update_bundle_versions.py around extras suffixes, both fatal
or silently wrong for the first nightly carrying the lfx-bundles
metapackage:
- update_root_pyproject_for_bundle's dep regex required the version
specifier immediately after the name, so "lfx-bundles[all]>=1.0,<2.0"
(a MAIN dep) was left unrewritten while the workspace member was
renamed to lfx-bundles-nightly; uv lock then tries to resolve stable
lfx-bundles from PyPI, where it does not exist. The same gap silently
left "lfx-docling[local]>=0.1.0" optional-dep refs pointing at the
stable distribution. The regex now tolerates an [extras] group and
carries it into the replacement.
- rename_bundle_pyproject skipped self-referencing extras, so the
metapackage's generated `all` extra kept 45 "lfx-bundles[<provider>]"
members after the rename, pulling the stable distribution (same
lfx_bundles import package, install collision) once published. Self
refs now follow the rename, idempotently.
Tests drive the real script module, mirroring test_bundle_lfx_pin.py.
This closes the PR-2 audit item flagged when the metapackage was
introduced. The canonical-pre-release cutover would retire the nightly
rename entirely; until it lands, the rename must be correct.
* feat(bundles): graduate partner set to standalone lfx-<provider> packages (#13573)
* feat(bundles): graduate partner set to standalone lfx-<provider> packages
Extracts the five partner/flagship providers into manifest-shipping
distributions: lfx-openai, lfx-anthropic, lfx-amazon, lfx-datastax,
lfx-cohere. Each ships extension.json (lfx.compat ["1"]), a
langflow.extensions entry-point, an lfx>=1.10.0,<2.0.0 pin, and is wired into
the root workspace marker blocks. Zero flow impact by the bundle-name
invariant: ext:<provider>:<Class>@official ids are unchanged.
Mechanics:
- adopts the five-phase scripts/migrate/port_bundle.py from
feat/bundle-mass-extraction (handles shared-base moves, consumer rewrites,
surgical index updates, migration-table appends) and runs it per partner
with --migration-release 1.11.0
- datastax's shared lfx.base.datastax moves into lfx_datastax.base; its
backend unit tests move to src/bundles/datastax/tests (74 pass); its ruff
per-file-ignore and its check_component_env_writes ALLOWLIST entry travel;
10 repo consumers rewritten (incl. the vector_store_rag starter project)
- amazon_bedrock_converse.py legacy langflow.* imports rewritten to the lfx.*
equivalents (thin re-export aliases; behavior-identical) pre-extraction
- runtime deps pinned from langflow-base's extras / direct deps (wrapper-
guaranteed SDKs stay transitive, parity-exact); --remove-base-extra dropped
the openai/anthropic/cohere/aws extras from langflow-base[complete] in favor
of the bundle pins; the astradb extra stays (also used outside datastax)
- in-tree dirs replaced with marker shims pointing at lfx_<p>.components.<p>
(skipped by the in-tree walk; legacy from lfx.components.<p> imports keep
working); lfx/components/__init__.py entries kept, consistent with the
consolidated providers
- validate.py: accept classes whose base is a *derived* Component base
(LCVectorStoreComponent / LCToolComponent / ...) -- they inherit the
class-level outputs declaration and only override the output method, which
the AST-only check could not see; bare Component subclasses keep the strict
build/outputs requirement (+ regression test)
- BUNDLE_API.md changelog entries added for the branch's surface changes: the
lfx.bundles manifest-less discovery group + precedence tier +
bundle-discovery-malformed code (from the foundation PR) and the validator
acceptance above
- 100 append-only migration entries (20 classes x 5 partners x 4 shapes);
component_index.json surgically updated 300->280 components / 60->55
modules, sha256 recomputed and verified
- detect-secrets baseline re-keyed for moved partner files
Dep parity: lock diff adds only the five lfx-* package names; no third-party
package added or removed. One benign transitive re-resolution: anthropic SDK
0.105.2 -> 0.109.0 (allowed by langchain-anthropic's range on both sides).
Verified: all five register at @official via the installed-manifest tier
(extension_id/distribution = lfx-<p>, 20 components); manifest-less
lfx_bundles unchanged (35 bundles, disjoint); all 5 shims import; engine-safe;
`lfx extension validate` passes for all five; 450 extension unit tests, 60
pilot-upgrade migration tests, 74 moved datastax tests pass; ruff clean.
* fix(bundles): shim lfx.base.datastax so stored-flow imports keep resolving
The datastax graduation moved lfx.base.datastax into lfx_datastax.base,
but saved flows and starter templates (Hybrid Search RAG,
TemplateAssistant) embed `from lfx.base.datastax.astradb_base import
AstraDBBaseComponent` inside their stored component code fields, which
is re-executed verbatim at flow build time. Without a shim that import
raises ModuleNotFoundError and the flows fail to build
(test-starter-projects red).
Mirror the lfx.components shim contract: module-aliasing to
lfx_datastax.base, narrow except that only translates a missing bundle
(transitive dep failures re-raise untouched), marker-tagged single-file
dir, removed at M4 together with the components shims.
* chore(bundles): bounded version ranges for curated lfx-* packages (#13576)
langflow's pyproject is the release-coordination point now that lfx and the
lfx-* bundles release independently. Every curated lfx-* dependency declares a
BOUNDED range (>=A,<B), never an exact pin -- exact pins in reusable library
metadata create resolver conflicts for downstreams that depend on a different
version. Exact pins for reproducibility live in uv.lock and the release-build
manifests.
- the 4 pilots + 5 graduated partners: >=0.1.0 -> >=0.1.0,<1.0.0 (bundles
follow their own 0.1.x cadence)
- lfx-bundles[all]>=1.0,<2.0 unchanged (versions with the metapackage
contract)
- the three lfx-docling[...] optional-dependency refs bounded the same way
- policy documented inline in the bundle-deps marker block; the cross-bundle
CI matrix verifies the ranges resolve together across the supported lfx axis
Verified: uv lock resolves with zero package/version changes (bounds are
metadata-only today); the nightly rename's dep regex already matches the
bounded form.
* test(bundles): lock the in-tree shim contract + breakage audit (#13577)
* chore(bundles): bounded version ranges for curated lfx-* packages
langflow's pyproject is the release-coordination point now that lfx and the
lfx-* bundles release independently. Every curated lfx-* dependency declares a
BOUNDED range (>=A,<B), never an exact pin -- exact pins in reusable library
metadata create resolver conflicts for downstreams that depend on a different
version. Exact pins for reproducibility live in uv.lock and the release-build
manifests.
- the 4 pilots + 5 graduated partners: >=0.1.0 -> >=0.1.0,<1.0.0 (bundles
follow their own 0.1.x cadence)
- lfx-bundles[all]>=1.0,<2.0 unchanged (versions with the metapackage
contract)
- the three lfx-docling[...] optional-dependency refs bounded the same way
- policy documented inline in the bundle-deps marker block; the cross-bundle
CI matrix verifies the ranges resolve together across the supported lfx axis
Verified: uv lock resolves with zero package/version changes (bounds are
metadata-only today); the nightly rename's dep regex already matches the
bounded form.
* test(bundles): lock the in-tree shim contract + breakage audit
Locks the five-point contract for the 50 lfx-bundles import shims under
lfx/components/ (45 metapackage + 5 graduated partners):
1. first line is the `# lfx-bundles-shim` marker (keys the in-tree walk's
double-registration skip);
2. a shim dir is exactly one __init__.py -- no impls, no deps;
3. sys.modules module-aliasing (submodule trees resolve);
4. bundle missing -> actionable ModuleNotFoundError whose wording is part
of the contract ("pip install lfx-bundles" / "pip install lfx-<p>");
5. a missing *transitive* dep re-raises untouched.
Test shape (env-adaptive by design -- the lfx test env prunes the venv, so
nothing here assumes bundles are installed):
- source-level sweep of every real shim, parameterized (no imports);
- hermetic mechanism tests against a synthetic shim + synthetic target
(alias-resolves / locked-message / transitive-reraise);
- the walk-skip detector and the marker sweep must agree exactly;
- one adaptive live test on lfx.components.tavily exercising whichever
branch the running env is in.
106 tests pass in the pruned lfx env (where the live test exercises the
bare-engine locked-message branch).
Breakage audit (gh code search, recorded in the PR): ~20-25 public repos
reference lfx.components.*; the majority are langflow forks (vendored tree,
unaffected by packaging). Genuine library consumers exist and the moved
providers have real surface (openai: 11 external repos, datastax: 17) --
covered by the shims wherever bundles are co-installed (every
`pip install langflow`). Decision: deprecation warnings NOT warranted now;
revisit at M4 (shim removal), where one loud minor release before removal
is the right shape.
* test(bundles): extend the shim contract sweep to lfx.base shims
The datastax graduation moved lfx.base.datastax into lfx_datastax.base
and left a module-aliasing shim behind (stored flow code fields embed
the legacy import and are re-executed verbatim at build time). Sweep
lfx/base for marker shims with the same source-level contract as the
components sweep: one-file stub, module-aliasing to the bundle's base
subpackage, narrow name-checked except, locked install message.
* docs(bundles): install shapes, override rule, bundle_api_version (#13578)
Documents the deliberately small user-facing rule surface of the metapackage
split (1.11):
- extensions-overview.mdx: the two install stories (pip install langflow =
everything, same as today; pip install lfx = engine only, bring your own
bundles); the bundle-name-is-identity invariant; the current package table
(lfx-bundles metapackage + the 5 partner packages + the 4 pilots); the
legacy-import shim behavior with the locked ModuleNotFoundError wording and
the migration recipe for direct lfx users (switch to lfx[bundles] or pin
lfx-<provider> packages); the override rule ("ship a manifest -- a manifest
always wins", with the bundle-shadowed warning); bundle_api_version as the
single compat number (lfx.compat ["1"]; manifest-less packages ride their
PEP 508 lfx pin) and the no-version-arithmetic rule.
- deployment-lfx-compatibility.mdx: lfx[bundles] as the headless/serverless
deployment footnote (engine + lfx-bundles[all]; slimmer per-provider
alternative; intentionally no lfx[all]).
Both files verified MDX-safe (no unbackticked angle brackets).
* fix(bundles): stack-review fixes — symlink containment, idempotency, docs accuracy (#13579)
* chore(bundles): bounded version ranges for curated lfx-* packages
langflow's pyproject is the release-coordination point now that lfx and the
lfx-* bundles release independently. Every curated lfx-* dependency declares a
BOUNDED range (>=A,<B), never an exact pin -- exact pins in reusable library
metadata create resolver conflicts for downstreams that depend on a different
version. Exact pins for reproducibility live in uv.lock and the release-build
manifests.
- the 4 pilots + 5 graduated partners: >=0.1.0 -> >=0.1.0,<1.0.0 (bundles
follow their own 0.1.x cadence)
- lfx-bundles[all]>=1.0,<2.0 unchanged (versions with the metapackage
contract)
- the three lfx-docling[...] optional-dependency refs bounded the same way
- policy documented inline in the bundle-deps marker block; the cross-bundle
CI matrix verifies the ranges resolve together across the supported lfx axis
Verified: uv lock resolves with zero package/version changes (bounds are
metadata-only today); the nightly rename's dep regex already matches the
bounded form.
* test(bundles): lock the in-tree shim contract + breakage audit
Locks the five-point contract for the 50 lfx-bundles import shims under
lfx/components/ (45 metapackage + 5 graduated partners):
1. first line is the `# lfx-bundles-shim` marker (keys the in-tree walk's
double-registration skip);
2. a shim dir is exactly one __init__.py -- no impls, no deps;
3. sys.modules module-aliasing (submodule trees resolve);
4. bundle missing -> actionable ModuleNotFoundError whose wording is part
of the contract ("pip install lfx-bundles" / "pip install lfx-<p>");
5. a missing *transitive* dep re-raises untouched.
Test shape (env-adaptive by design -- the lfx test env prunes the venv, so
nothing here assumes bundles are installed):
- source-level sweep of every real shim, parameterized (no imports);
- hermetic mechanism tests against a synthetic shim + synthetic target
(alias-resolves / locked-message / transitive-reraise);
- the walk-skip detector and the marker sweep must agree exactly;
- one adaptive live test on lfx.components.tavily exercising whichever
branch the running env is in.
106 tests pass in the pruned lfx env (where the live test exercises the
bare-engine locked-message branch).
Breakage audit (gh code search, recorded in the PR): ~20-25 public repos
reference lfx.components.*; the majority are langflow forks (vendored tree,
unaffected by packaging). Genuine library consumers exist and the moved
providers have real surface (openai: 11 external repos, datastax: 17) --
covered by the shims wherever bundles are co-installed (every
`pip install langflow`). Decision: deprecation warnings NOT warranted now;
revisit at M4 (shim removal), where one loud minor release before removal
is the right shape.
* docs(bundles): install shapes, override rule, bundle_api_version
Documents the deliberately small user-facing rule surface of the metapackage
split (1.11):
- extensions-overview.mdx: the two install stories (pip install langflow =
everything, same as today; pip install lfx = engine only, bring your own
bundles); the bundle-name-is-identity invariant; the current package table
(lfx-bundles metapackage + the 5 partner packages + the 4 pilots); the
legacy-import shim behavior with the locked ModuleNotFoundError wording and
the migration recipe for direct lfx users (switch to lfx[bundles] or pin
lfx-<provider> packages); the override rule ("ship a manifest -- a manifest
always wins", with the bundle-shadowed warning); bundle_api_version as the
single compat number (lfx.compat ["1"]; manifest-less packages ride their
PEP 508 lfx pin) and the no-version-arithmetic rule.
- deployment-lfx-compatibility.mdx: lfx[bundles] as the headless/serverless
deployment footnote (engine + lfx-bundles[all]; slimmer per-provider
alternative; intentionally no lfx[all]).
Both files verified MDX-safe (no unbackticked angle brackets).
* fix(bundles): review fixes — symlink containment, idempotency, docs accuracy
Fixes from the multi-agent stack review (kept as a separate PR so the
reviewed PRs' diffs stay frozen for QA's independent pass):
- _bundles_root.py: directory-level symlink containment — a provider dir
that resolves outside the bundle root is skipped with a typed
bundle-discovery-malformed warning, mirroring the seed-directory walk's
is_within rule (+ regression test). Anchors the trust boundary to the
installed package tree.
- consolidate_bundles.py: migration-append idempotency guard — entries are
deduped on full content so a partially-failed earlier run cannot duplicate
rows on re-run (table stays append-only).
- langflow-base pyproject: documented WHY the aws extra is deliberately
retained after the lfx-amazon graduation (boto3 also backs the server's
own S3 storage backend and lfx's S3 ingestion — review suggested removing
it; that would regress S3 storage support).
- extensions-overview.mdx: note that `lfx extension list` shows
manifest-shipping extensions only; manifest-less packages load at startup
but are not listed.
93 loader+migration tests pass (incl. the new symlink test); ruff clean.
* refactor(bundles): stabilize import_mod as a public BUNDLE_API utility
The lazy-import helper that bundle packages call from their
__getattr__-based __init__.py files lived at lfx.components._importing -- an
internal path with no stability contract, imported by 35 separately-installed
bundle __init__ files (30 lfx-bundles providers + the 5 graduated partners).
- canonical home is now lfx.utils.lazy_import.import_mod (code unchanged);
lfx.components._importing re-exports it so in-tree callers and any external
code on the old path keep working
- all 35 bundle-side imports rewritten to the stable path
- BUNDLE_API.md: surface table entry + changelog (additive)
- contract tests: re-export identity, both call forms, the AttributeError
conversion
Verified: identity holds across both paths; lazy __init__ loads work through
the new path for both bundle families; 110 tests pass; ruff clean.
* fix(bundles): tombstone the broken legacy ZepChatMemory build method (#13580)
build_message_history targeted the zep-python v1 SDK (ZepClient +
zep_python.langchain.ZepChatMessageHistory); both were removed in
zep-python 2.x and the zep extra pins 2.0.2, so the method has been
unable to run for as long as the pin has existed -- its ImportError
guard misleadingly told users to 'pip install zep-python' (already
installed). The component is legacy=True with helpers.Memory as its
designated replacement, so rather than hand-write a new integration
against the 2.x SDK, the method now raises a clear RuntimeError
pointing at the Message History component.
Flow identity is preserved: class/component name, display_name,
description, inputs and the memory output are byte-identical, so saved
flows keep loading, i18n locale keys are unchanged, and
migration_table.json needs no edits. New bundle tests pin the stub
contract (identity, actionable error, no zep_python import).
* test(bundles): extend the shim contract sweep to lfx.base shims
The datastax graduation moved lfx.base.datastax into lfx_datastax.base
and left a module-aliasing shim behind (stored flow code fields embed
the legacy import and are re-executed verbatim at build time). Sweep
lfx/base for marker shims with the same source-level contract as the
components sweep: one-file stub, module-aliasing to the bundle's base
subpackage, narrow name-checked except, locked install message.
* test(bundles): extras-drift guard for the lfx-bundles metapackage
Risk-2 of the metapackage split: the generated `all` extra and the
per-provider extras must never drift by hand-edit, or `pip install
langflow` silently loses a provider's deps. Guard the four invariants:
extras <-> provider dirs (PEP 685-normalized), `all` == the exact
self-ref set, normalized keys collision-free, and the metapackage
provider set disjoint from the graduated partner distributions.
* fix(extension): symlink-escaped providers reject with path-escape, matching the documented contract
The lfx.bundles provider containment check (stack-review symlink fix)
emitted bundle-discovery-malformed, whose template and hint describe a
broken entry-point declaration. The changelog's path-safety entry
already documents symlink escapes as path-escape on every other
discovery path; use the same code here. Also resolves the semantic
merge conflict with the per-mode code split (the removed
_malformed_error location kwarg).
* fix(tests): repoint lfx tests off moved providers; complete provider fallback map
CI fail-fast had been masking these: the LFX test job runs in an
engine-only env where openai/anthropic/chroma are now bundle-package
shims, so every test that used them as the example category failed on
all Python versions (3.14 just reported first), and the backend Group 2
leg failed collecting test_lfx_bundles_extras.py on Python 3.10.
- flow_requirements: complete _PROVIDER_PACKAGE_FALLBACKS for the nine
moved model providers (OpenAI, Anthropic, Amazon Bedrock, Groq,
Google Generative AI, SambaNova, IBM watsonx.ai, Ollama + existing
Azure OpenAI). In engine-only installs MODEL_PROVIDERS_DICT registers
only in-tree providers, so the dynamic source-inspection path cannot
resolve moved providers; the static fallbacks keep lfx run/serve
requirements inference working. A dict hit still wins.
- test_dynamic_imports / test_import_utils: use composio (still-in-tree
lazy category with its SDK absent in the bare env) as the example
category instead of openai/anthropic/chroma; patch import_module at
its canonical home lfx.utils.lazy_import.
- flow-builder tests (build_flow_from_spec, flow_builder_tools,
propose_field_edit): specs use LanguageModelComponent (in-tree)
instead of OpenAIModel.
- test_lfx_bundles_extras: tomli fallback for Python 3.10 (tomllib is
stdlib 3.11+; pytest guarantees tomli on <3.11).
Full lfx unit suite: 4550 passed. Backend extras+pin tests: 24 passed.
* fix(tests): repoint MCP client-server tests off graduated OpenAIModel key
Same fail-fast-masked class as 14dfa7f268, backend side: since the
partner graduation, extension components register in /api/v1/all under
their namespaced ids (ext:openai:OpenAIModelComponent@official), so the
bare 'OpenAIModel' key these tests hardcoded no longer exists on any
Python version (the 'py3.14-only' Group 2 failure was the only leg that
ran; 3.10 was cancelled by fail-fast). The MCP feature itself is fine —
search returns the namespaced ids and add_component accepts them.
- Use LanguageModelComponent (in-tree, stable bare key; SecretStr
api_key, real_time_refresh fields, advanced 'stream', LanguageModel
output) for the redaction/configure/search/describe/spec tests.
- configure_dynamic_field: model_name -> api_key (the refresh field on
LanguageModelComponent).
- prompt-template-variables spec: drop the model node — server-side
validation builds the graph and a model without an API key fails its
build; the test's subject is the dynamic {var} fields.
Full file: 69 passed.
* Update test_mongodb_atlas.py
* Frontend tests
24 KiB
Bundle API
Stable surface that Langflow Extension Bundles consume. Every public symbol
listed below is part of the contract: changes to its name, signature, semantics,
or visibility require a coordinated version bump and a ## Changelog entry.
This document is paired with the integer BUNDLE_API_VERSION declared in
lfx.extension.manifest. Manifests
declare the contract versions they support via lfx.compat: ["1"]; a bundle
that does not list str(BUNDLE_API_VERSION) is rejected at install time with
version-constraint-unsatisfied.
CI gate: any PR that modifies a file containing an in-scope surface MUST add a
## Changelogentry describing the change. The CI guardscripts/migrate/check_bundle_api_changelog.pyenforces this. Pure-internal refactors that preserve every public symbol's name and signature do not require a changelog entry, but reviewers should be skeptical.
Surface (v0)
Component base class
| Symbol | Source |
|---|---|
Component |
lfx.custom.custom_component.component.Component |
Component.build() (declared on subclasses) |
call site of every loaded bundle module |
Component.inputs |
declarative input list |
Component.outputs |
declarative output list |
Component.display_name / Component.description / Component.icon / Component.documentation |
metadata read by the palette |
Component.name |
optional override of the registry class name |
Inputs
| Symbol | Source |
|---|---|
Input (base) |
lfx.io |
MessageTextInput / MultilineInput / SecretStrInput |
lfx.io |
IntInput / FloatInput / BoolInput |
lfx.io |
DropdownInput / TabInput |
lfx.io |
DictInput / NestedDictInput |
lfx.io |
FileInput / LinkInput |
lfx.io |
HandleInput |
lfx.io |
Outputs
| Symbol | Source |
|---|---|
Output |
lfx.io |
Schema types
| Symbol | Source |
|---|---|
Data |
lfx.schema.data |
DataFrame |
lfx.schema.dataframe |
Message |
lfx.schema.message |
Lazy-import helper (consumed by bundle __init__.py files)
| Symbol | Source |
|---|---|
import_mod(attr_name, module_name, package) |
lfx.utils.lazy_import (canonical; lfx.components._importing re-exports it for backward compatibility) |
Manifest contract (consumed by the loader)
| Symbol | Source |
|---|---|
Manifest schema (extension.json / [tool.langflow.extension]) |
lfx.extension.manifest.ExtensionManifest |
BundleRef (one entry in bundles[]) |
lfx.extension.manifest.BundleRef |
LfxCompat (declared as manifest.lfx) |
lfx.extension.manifest.LfxCompat |
BUNDLE_API_VERSION (the integer this lfx ships) |
lfx.extension.manifest |
EXTENSION_SCHEMA_URL / SCHEMA_VERSION |
lfx.extension.manifest |
Slot vocabulary: official (installed pip distributions and seed
directories) and extra (paths declared in LANGFLOW_COMPONENTS_PATH).
Component IDs at runtime are ext:<bundle>:<Class>@<slot>.
Discovery + loading entry points
| Symbol | Source |
|---|---|
load_extension(root) |
lfx.extension.loader |
load_installed_extensions() |
lfx.extension.loader |
discover_inline_bundles() |
lfx.extension.loader |
discover_installed_extensions() / discover_seed_extensions() / discover_all_extensions() |
lfx.extension.discovery |
LoadedComponent |
lfx.extension.loader (frozen dataclass; what the registry stores) |
LoadResult |
lfx.extension.loader |
SLOT_OFFICIAL / SLOT_EXTRA |
lfx.extension.loader |
Reload pipeline
| Symbol | Source |
|---|---|
reload_bundle(registry, bundle_name) |
lfx.extension.reload |
BundleRegistry |
lfx.extension.bundle_registry |
BundleRecord |
lfx.extension.bundle_registry |
ReloadInProgressError |
lfx.extension.bundle_registry |
POST /api/v1/extensions/{id}/bundles/{name}/reload |
langflow.api.v1.extensions |
Errors
| Symbol | Source |
|---|---|
ExtensionError |
lfx.extension.errors |
ExtensionErrorCollection |
lfx.extension.errors |
format_extension_error(error) |
lfx.extension.errors |
ERROR_CODES (frozenset of every typed code) |
lfx.extension.errors |
The full kebab-case discriminant set is the contract — adding a code is
backward-compatible; removing or renaming a code is a breaking change and
requires a BUNDLE_API_VERSION bump.
Validate / authoring CLI
| Symbol | Source |
|---|---|
validate_extension(root, *, execute_imports=False) |
lfx.extension.validate |
ValidateReport |
lfx.extension.validate |
lfx extension validate (CLI) |
lfx.cli._extension_commands |
lfx extension schema (CLI) |
lfx.cli._extension_commands |
lfx extension init (CLI) |
lfx.cli._extension_commands |
lfx extension dev (CLI -- registers a local path and execs langflow run) |
lfx.cli._extension_commands |
lfx extension list (CLI) |
lfx.cli._extension_commands |
lfx extension reload (CLI) |
lfx.cli._extension_commands |
register_dev_extension / unregister_dev_extension (Python API) |
lfx.extension.dev_registry |
Migration
| Symbol | Source |
|---|---|
| Migration-table file | src/lfx/src/lfx/extension/migration/migration_table.json |
MigrationEntry |
lfx.extension.migration.schema |
MigrationTable |
lfx.extension.migration.schema |
migrate_flow_payload(payload, table) |
lfx.extension.migration.rewrite |
MIGRATION_SCHEMA_VERSION |
lfx.extension.migration.schema |
Out of scope (v0)
These are reserved in the manifest schema and produce a typed
field-deferred-in-this-milestone error if set; they are NOT part of the
v0 contract:
services— bundle-declared service factoriesroutes— bundle-mounted HTTP routeshooks— bundle-declared lifecycle hooksstarter_projects— bundle-shipped starter flowsuserConfig— bundle-declared user-config schema- Multi-bundle manifests (
bundleslist with length > 1)
Pilot bundle: lfx-duckduckgo
The shipped LE-1023 pilot is duckduckgo, extracted into the
standalone distribution
lfx-duckduckgo under src/bundles/duckduckgo/
with its own pyproject.toml. langflow's own pyproject.toml
declares lfx-duckduckgo>=0.1.0 as a regular dependency so a flat
pip install langflow continues to ship the bundle as before.
Why this bundle:
- Single component (
DuckDuckGoSearchComponent) in a single file (duck_duck_go_search_run.py). - Zero git churn over the last six months.
- Modern
Componentbase class (noLCToolComponentlegacy). - No authentication required — failure mode is a single failed request, not a paid-API outage.
- Class name is globally unique across
src/lfx/src/lfx/components/**, so the bare-name migration entry is allowed bycheck_bare_names.py.
The runtime half of the M1 proof-of-delivery gate (save a flow on
pre-migration Langflow, upgrade, confirm it loads AND runs identically)
lives in the dogfood checklist at
src/bundles/duckduckgo/M1_DOGFOOD_CHECKLIST.md;
the deserialize half is covered by
src/lfx/tests/integration/extension/test_pilot_duckduckgo_upgrade.py.
Changelog
v0 (this release)
- Initial surface enumerated above. Frozen as
BUNDLE_API_VERSION = 1. BundleRegistry.write_locked()exposed as a public context manager so the reload pipeline can hold the registry write lock across both thesys.modulesswap and theBundleRecordinstall. Concurrent readers can no longer observe new modules paired with the old record. No change to the addressable component contract.- HTTP reload endpoint (
POST /api/v1/extensions/{id}/bundles/{name}/reload) returns422 Unprocessable Entityfor structural failures (broken bundle, missing source path, name mismatch) instead of200 OKwithok=false. Body is{...primaryError, result: ReloadResult}so the full typed result is preserved under the FastAPIdetailenvelope.409 Conflictforreload-in-progressis unchanged. - CLI table updated to remove the obsolete
dev register/dev unregister/dev listsubcommands; the actual surface isextension dev <path>plus the Python helpersregister_dev_extension/unregister_dev_extension. MigrationTable.ambiguous_bare_namesadded. Each entry is{name, candidates: [list of canonical IDs]}and registers a bare class name that exists in 2+ bundles. The deserializer now surfacescomponent-name-ambiguous(with the candidate targets) for any bare name listed here, instead of falling through to the genericcomponent-not-found-with-hint. Seeded with the canonical regression cases (MergeDataComponent,SplitTextComponent,SubFlowComponent).check_bare_names.pynow verifies every Component class found in 2+ bundle folders has a matching marker, so a future bundle move that introduces a new ambiguity is caught at PR time.- Router-trust CI guard broadened to scan every
.pyundersrc/backend/base/langflow/api/**andsrc/lfx/src/lfx/**; a new file that mounts anAPIRouter(prefix=".../extensions...")is auto-detected and checked for forbidden install/uninstall/registry-mutation handlers. Authors of files with non-literal prefixes can opt in via a# router-trust: in-scopemarker. - Router-trust guard rewritten to use AST-based cross-file resolution.
A forbidden handler in module A is now caught when module B mounts A's
router via
parent.include_router(child, prefix=".../extensions..."), and the same applies transitively across multi-hop include_router chains. An imported router that cannot be statically resolved is ignored (the guard never flags routes it cannot prove reachable from/extensions); routes co-located with an in-scope router ARE flagged. check_migration_append_only.pynow comparesambiguous_bare_namesalongsideentries. A marker may not be removed once published, and itscandidateslist may only grow -- shrinking it would silently regress flows fromcomponent-name-ambiguoustocomponent-not-found-with-hint.- Router-trust guard now resolves dotted attribute references in
include_routerand decorators.include_router(child.api.router, prefix="/extensions")afterimport child.api(and theimport child.api as alias; alias.routershape) are caught -- not justfrom child.api import router as child_router. The parser flattens anyName/Attributechain, and the resolver walks imports of either kind (from M import Nandimport M, with or without an asname) back to the source file. - Router-trust guard's relative-import resolver is now
__init__.py-aware. Inside a package,from .child import Yanchors at the package itself (level=1 ->pkg); inside a regular modulepkg.fooit anchors at the parent package (level=1 ->pkg). The arithmetic differs because__init__.py's file module IS the package, whilepkg/foo.py's file module ispkg.foo. The resolver tracksis_packageand decrementslevelby one for__init__.pyfiles so both shapes resolve correctly. - Code-review hardening pass across the extension subsystem. No public
symbol's name or signature changed; this entry covers behavioural
tightening that bundle authors and operators should be aware of:
- Path-safety contract honored on every discovery path.
DiscoveredExtensionrecords emitted fromdiscover_installed_extensions/discover_seed_extensionsnow run the same resolve-and-relative_tocontainment check thatvalidate_extensionperforms. A symlinkedbundles[0].pathor a symlinked seed subdirectory that escapes the extension root is now rejected withpath-escapebefore reaching the loader, instead of slipping through toexec_module(). The shared primitive lives atlfx.extension._paths.is_within; every walker (loader, validator, seed discovery, inline-bundle discovery) uses the same function and the sameSKIP_DIR_NAMES. --execute-importsenv allowlist. The validator's--execute-importssubprocess now inherits an explicit allowlist (PATH,LANG,LC_*,SYSTEMROOT,TMPDIR,TZ, Python locale + encoding vars) instead of denylisting onlyLANGFLOW_*/LFX_*. Cloud / CI credentials (AWS_*,OPENAI_API_KEY,GITHUB_TOKEN, ...) no longer propagate into untrusted bundle import. The CLI / module docs re-frame this pass as best-effort hygiene lint, not a sandbox.- AST hygiene lint widened.
_find_top_level_ionow flagsexec,eval,__import__,compileas top-level primitives andimportlib.import_module/importlib.__import__as dotted-name primitives. Still best-effort literal-name matching; trivially bypassable by obfuscation, and documented as such. - Reload swap is non-destructive.
_swap_sys_modulesnow builds the staging->prod rename map before anysys.modulesmutation, snapshots popped old modules into a recovery map, and restores them on any mid-swap exception. The length-mismatch tripwire onzip(strict=True)no longer leaves the prod namespace shredded. A new typed code,reload-class-retag-failed, is appended toReloadResult.warningswhencls.__module__cannot be retagged so the empty-palette-after-reload regression leaves a trail instead of silently failing. - Cross-source bundle-name collision.
load_installed_extensionsnow detects two distributions with different canonical names but identicalbundle.name(which would silently clobber each other at_lfx_ext.official.<name>.*) and emits a typedduplicate-bundle-nameerror on the loser, dropping its components.BundleRegistry.install_bundleadditionally logs a WARNING when an existing record is replaced by a record from a differentsource_path(catches collisions the upstream precedence resolver missed). - Reload endpoint off event loop.
POST /api/v1/extensions/{id}/bundles/{name}/reloadnow invokesreload_bundleviaasyncio.to_threadso slow or large bundle imports do not freeze the worker for other in-flight requests. The wire contract (status codes, body shape) is unchanged. - Stable typed-error code rename.
multi-bundle-deferred-in-this-milestoneis renamed to the stablemulti-bundle-unsupported. The old code is retained inERROR_CODESas a deprecated alias for one milestone for log scrapers. Three new codes are added toERROR_CODES:duplicate-bundle-name(see above),reload-class-retag-failed(see above), andreload-transport-error(CLI-side connectivity failure, previously misreported asreload-source-missing). - Discovery preserves "unreadable" vs "absent" distinction.
_pyproject_declares_extensionnow propagatesOSErrorso a permission failure on a pyproject that might declare an extension surfaces asmanifest-unreadableinstead of being silently dropped as "no extension here". - Dev registry corruption is logged.
_read_statenow distinguishes file absent (silent, legitimate empty registry), file present but unreadable (WARNING), and file present but corrupt JSON / wrong shape (WARNING with detail). The state file is written with mode 0600 so a hostile third-party process cannot inject an extension path into the developer's nextlangflow run. - Entry-point predicate avoids module-level side effects.
_entry_point_loads_to_componentnow consultsimportlib.util.find_specfirst and only falls through toep.load()when the spec lookup is insufficient. Theexcept BaseExceptionwas narrowed toexcept ExceptionsoSystemExit/KeyboardInterruptare no longer swallowed at filter time. - Frontend reload-success warnings surfaced. The reload route's
ReloadResult.warnings(non-empty on success) now reach the user via a notice toast in addition to the green success toast. Wire shape unchanged; this is a UI fix that consumes existing payload fields. - Internal-only file split.
sys.modulessurgery primitives moved tolfx.extension.reload_swap;load_installed_extensions/load_seed_extensionsmoved tolfx.extension.loader._startup. Both are re-exported from their previous import paths so external imports are unchanged. - Editable installs are discovered via the entry-point fallback.
_distribution_manifest_pathnow falls back to thelangflow.extensionsentry-point group whendist.filesonly surfacesdist-info/entries (thepip install -e/uv pip install -ecase). The entry-point value is resolved viaimportlib.util.find_spec-- which runs import-system finders but never executes the module body -- and the resulting package directory is scanned forextension.jsonor a[tool.langflow.extension]pyproject. Wheel installs are unaffected: the fallback only fires when the primarydist.filesscan finds no manifest. Previously, editable-installed bundles were silently dropped bylfx extension listand the registry, even though the bundle pyproject already declared the entry-point. - Reload CLI:
--bundleis optional;--allis implemented.lfx extension reload <ext_id>now resolves the bundle name from localdiscover_all_extensionswhen--bundleis omitted; explicit--bundlestill wins for cases where the local install is not visible to the running server.lfx extension reload --alliterates every locally-discovered bundle, POSTs reload to each, and exits non-zero if any reload fails (previously hard-errored as "not yet wired").--allis mutually exclusive with a positional id /--bundle(exit 2). The HTTP wire contract (POST /api/v1/extensions/{id}/bundles/ {name}/reloadper-bundle) is unchanged; this is a CLI-only surface change.
- Path-safety contract honored on every discovery path.
- User-scoped extension events. Bundle lifecycle events
(
bundle_reloaded,bundle_reload_failed,flow_migrated,extension_error) now publish to a per-user keyspace (user:<user_id>) instead of the shared"global"bucket so flow-migration and reload payloads cannot leak across users via the poll endpoint.reload_bundlegains an optional keyword-onlyuser_id: str | None = Noneargument. When supplied,bundle_reloaded/bundle_reload_failedevents are emitted to keyspaceuser:<user_id>;None(CLI / authless dev) keeps the legacy"global"emission. Existing positional callers are unaffected.POST /api/v1/extensions/{id}/bundles/{name}/reloadnow resolves the authenticated user and threads its id intoreload_bundle, so every reload triggered via HTTP is published to that user's keyspace. Wire contract (status codes, body shape) unchanged.GET /api/v1/extensions/eventsdrops its client-suppliedkeyspacequery parameter. The endpoint derives the keyspace from the authenticated user server-side, so an authenticated client can no longer poll another user's keyspace. Frontends that polled withoutkeyspace(the in-tree consumer) are unaffected; third-party callers that explicitly passedkeyspace=...will now receive422from FastAPI's strict parameter validation.
- Reload event payload aligned with
ReloadResult. Bothbundle_reloadedandbundle_reload_failedevents now carry the fullReloadResult.to_dict()envelope (ok,bundle,reload_id,components_added,components_removed,components_changed,warnings,errors) instead of a hand-rolled subset. Polling clients can now (a) detect body-only edits viacomponents_changedinstead of mis-reporting them as "no source changes detected", and (b) surface a failed reload'serrors[0].messageinstead of degrading to a generic "check server logs" fallback. HTTP response shape unchanged. GET /api/v1/extensions/eventsrejectskeyspaceexplicitly. Previously the endpoint accepted but silently ignored any client-suppliedkeyspacequery parameter (server-derived from the authenticated user since the prior entry). Silent drop masked client bugs that assumed the value had effect. The route now returns422 Unprocessable Entitywith a typedextension-events-keyspace-forbiddenerror envelope when the parameter is present.extension-events-keyspace-forbiddenis added toERROR_CODES(additive; codes-as-contract semantics preserved). In-tree polling clients that never sent the parameter are unaffected.- Manifest-less
lfx.bundlesdiscovery (metapackage split, 1.11). A distribution may declare[project.entry-points."lfx.bundles"]whose value is an importable package; each immediate subdirectory is loaded as one bundle at the@officialslot with noextension.json(the langchain-community model; these directories are not inputs tolfx extension validate-- the validator requires anextension.jsonor[tool.langflow.extension]manifest and reportsmanifest-not-foundwithout one).load_lfx_bundles_extensionsis exported fromlfx.extension(additive). Discovery precedence for cross-source bundle-name collisions becomesinstalled > seed > lfx_bundles > dev > inline-- manifest always wins, so a manifest-shippinglfx-<provider>shadows the same-named provider in a metapackage with the existingbundle-shadowedwarning (graduation requires no lockstep release). A metapackage provider whose name is already claimed by an installed or seed source is never imported -- all@officialsources share the_lfx_ext.official.<bundle>.*sys.modules namespace, so importing the losing copy would overwrite the winner's live modules; the skipped copy carries the same typedbundle-shadoweddiagnostic (onerrors, matching the resolver) so filtering by code never mixes severities. Namespace packages are walked across all portions, and resolved roots are deduplicated by path so duplicate declarations never self-shadow. New warning-only codes added toERROR_CODES(additive), one per discovery failure mode and none of which abort startup:bundle-discovery-malformed(declaration does not resolve to an importable package directory -- including a parent package whose__init__raises duringfind_spec),bundles-provider-name-invalid(provider folder is not a valid bundle name),bundles-root-unreadable(root cannot be enumerated), andduplicate-lfx-bundles-provider(same provider name in more than one root; first wins). Manifest-less bundles bypass theversion-constraint-unsatisfiedAPI-version gate by construction (no manifest to carrylfx.compat); install-time compatibility rides on the metapackage's PEP 508lfx>=X,<Ypin instead. Manifest-less records register withmanifestless=True(additive field onLoadResult/BundleRecord) and are not hot-reloadable: the reload pipeline refuses them with the new typed codereload-manifestless-unsupported(additive inERROR_CODES) instead of failingmanifest-not-found; pick up metapackage changes by upgrading the distribution and restarting the process. import_modpromoted to a stable public home. The lazy-import helper that bundle packages call from their__getattr__-based__init__.pyfiles moved from the internallfx.components._importingtolfx.utils.lazy_importand is now part of the BUNDLE_API surface (name, signature, and semantics contract-stable).lfx.components._importingre-exports it unchanged, so existing in-tree and third-party callers are unaffected (additive).- Validator accepts inherited entry-points.
validate_extensionno longer emitsbuild-method-missingfor a Component subclass whose base is a derived Component base (name ends withComponent, e.g.LCVectorStoreComponent/LCToolComponent): such classes inherit the class-leveloutputsdeclaration and only override the output method, which the AST-only check cannot resolve across modules. Direct subclasses of the rootComponentkeep the strict inlinebuild-or-outputsrequirement. Surfaced by the partner graduations (lfx-datastaxet al.); previously-passing bundles are unaffected (strictly fewer false positives).