44 KiB
Feature: Langflow Local Model (Bundled, Zero-Config Local LLM Provider)
Generated on: 2026-05-06 Status: Draft Owner: cristhianzl
Table of Contents
- Overview
- Ubiquitous Language Glossary
- Domain Model
- Behavior Specifications
- Architecture Decision Records
- Technical Specification
- Observability
- Deployment & Rollback
- Architecture Diagrams
- Platform Compatibility
1. Overview
Summary
The Langflow Local Model feature ships Langflow with a built-in, zero-configuration local LLM provider. New users can open any starter project and run it without supplying an OPENAI_API_KEY (or any other third-party credential). Under the hood, the provider wraps a curated local model (qwen2.5:1.5b) served by an embedded Ollama backend that is detected, installed, started, and pulled automatically on the user's machine.
Business Context
Today, every starter project, agent, and unified Language Model component depends on a paid third-party API key. This creates critical onboarding friction:
- A fresh installation of Langflow cannot run a single starter project until the user creates an external account.
- Evaluators and proof-of-concept users cannot see Langflow's value before they commit to OpenAI billing.
- Air-gapped, corporate, and offline environments cannot use Langflow at all.
- Demos and CI suffer from rate limits and per-call costs.
The Langflow Local Model removes this barrier: from first-launch to first successful flow run, no external account or API key is required.
Bounded Context
Local Model Lifecycle — owns everything related to detecting, installing, starting, and pulling the bundled local LLM backend. Lives in:
lfx.base.models.langflow_local_*(provider registration, validated wrapper class)lfx.services.local_model.*(platform detection, install orchestrator, model puller, bootstrap)langflow.api.v1.local_model(REST adapter)langflow.services.local_model.startup_hook(server lifespan integration)
Related Contexts
| Context | Relationship |
|---|---|
Unified Models (lfx.base.models.unified_models) |
Customer / Supplier — Langflow Local Model registers itself as a provider in the unified catalog and depends on its dispatch contract. |
| Ollama (third-party process) | Conformist — we conform to Ollama's HTTP API and CLI contract; we do not own it. URL-pinned and version-tolerant. |
LangChain (langchain-ollama) |
Conformist — ChatLangflowLocal extends ChatOllama. |
Starter Projects (langflow.initial_setup.starter_projects) |
Customer / Supplier — every LanguageModelComponent node has its model.value rewritten to point at the curated default. |
2. Ubiquitous Language Glossary
| Term | Definition | Code Reference |
|---|---|---|
| Langflow Model | The bundled, zero-config local LLM provider visible in the Model Providers UI. Always the first item in the unified catalog. | LANGFLOW_LOCAL_PROVIDER_NAME = "Langflow Model" |
| Curated Model | A model name explicitly allowed in the Langflow Local catalog. Today this is only qwen2.5:1.5b. Acts as both an allow-list (anti-DoS) and a documentation surface. |
CURATED_MODEL_NAMES, LANGFLOW_LOCAL_DEFAULT_MODEL |
| Allowed Base URL | An HTTP base URL that ChatLangflowLocal is allowed to talk to. Acts as the SSRF guard. |
ALLOWED_BASE_URLS (frozenset) |
| Consent Callback | A function the orchestrator calls before any side-effecting installer step. Returning False means the user declined and the pipeline must not proceed. |
ConsentCallback, installer.install(consent_callback) |
| Install Outcome | The typed result of an installer attempt: SUCCESS, DECLINED, FAILED, UNSUPPORTED, or ALREADY_INSTALLED. | InstallStatus, InstallOutcome |
| Pull Outcome | The typed result of a model pull: SUCCESS, ALREADY_PRESENT, FAILED, NETWORK_ERROR, or REJECTED. | PullStatus, PullOutcome |
| Bootstrap Outcome | The terminal status of ensure_local_model_ready: READY, DOCKER_GUIDANCE, INSTALL_DECLINED, INSTALL_FAILED, OLLAMA_NOT_RUNNING, PULL_FAILED, or UNSUPPORTED_OS. |
BootstrapStatus, BootstrapOutcome |
| Live Model Provider | A provider whose model list is resolved at runtime via /api/tags. Langflow Model is not a live provider — its list is curated and static. |
LIVE_MODEL_PROVIDERS |
| Marker Variable | The single non-secret variable attached to the provider so the existing frontend "activate" flow can record is_enabled=True without prompting for credentials. |
LANGFLOW_LOCAL_BASE_URL |
| Auto-Pull On Demand | The synchronous fallback inside ChatLangflowLocal.__init__ that pulls the curated model the first time the component is instantiated if the model is not yet on disk. |
_ensure_model_available, auto_pull parameter |
| Startup Hook | The FastAPI lifespan task that schedules ensure_local_model_ready in the background when the Langflow server boots. |
schedule_local_model_bootstrap |
3. Domain Model
3.1 Aggregates
LangflowLocalProvider (Configuration Aggregate)
- Root Entity: registry tuple
(LANGFLOW_LOCAL_PROVIDER_NAME, MODEL_PROVIDER_METADATA["Langflow Model"], LANGFLOW_LOCAL_MODELS_DETAILED, ChatLangflowLocal) - Value Objects:
ALLOWED_BASE_URLS(frozenset),CURATED_MODEL_NAMES(frozenset),LANGFLOW_LOCAL_DEFAULT_MODEL(str) - Invariants:
- The provider name is exactly
"Langflow Model"everywhere it appears (UI, metadata, class registry). LANGFLOW_LOCAL_DEFAULT_MODEL ∈ CURATED_MODEL_NAMES.- At least one model is marked
default=Truein the curated list. - The provider has at most one variable, which must be
is_secret=Falseandrequired=False. - The provider is not in
LIVE_MODEL_PROVIDERS(its list is curated, not dynamic). - The Langflow Model group is the first non-empty group returned by
get_models_detailed().
- The provider name is exactly
ChatLangflowLocal (Runtime Aggregate)
- Root Entity:
ChatLangflowLocal(ChatOllama) - Value Objects:
model: str,base_url: str - Invariants:
- Construction with a
base_urloutsideALLOWED_BASE_URLSraisesUnsafeBaseUrlError. - Construction with a
modeloutsideCURATED_MODEL_NAMESraisesUncuratedModelError. - If
langchain-ollamais missing, construction raisesLangchainOllamaMissingErrorwith an actionable message. ChatLangflowLocalis a true subclass ofChatOllama(Liskov substitutable — every place that acceptsChatOllamaacceptsChatLangflowLocal).- When
auto_pull=True(default), construction tries to pull the model if it is not yet on disk; pull failures are swallowed silently so the original 404 surfaces from the actual invoke.
- Construction with a
LocalModelInstaller (Strategy Aggregate)
- Root Entity:
InstallerProtocol - Strategies:
LinuxInstaller,MacOSInstaller,WindowsInstaller,DockerInstaller,_UnsupportedOSInstaller - Invariants:
- Every concrete strategy MUST honor: no side effect happens before
consent_callback(url) → True. - Every subprocess call uses list-args (no
shell=True) and an explicit timeout. - Every download URL is
https://ollama.com/...(HTTPS pinning). DockerInstallernever callsconsent_callback— it short-circuits toUNSUPPORTEDwith guidance.- The factory
get_installer()checksis_docker()beforesystem_name()so containerized Linux is never treated as host Linux.
- Every concrete strategy MUST honor: no side effect happens before
LocalModelBootstrap (Process Aggregate)
- Root Entity:
ensure_local_model_ready(async function) - Invariants:
- The pipeline runs the four checks in order:
is_docker → installer → ollama running → model pulled. Each gate has exactly one terminal status. - Re-install is never triggered if Ollama is already detected (no surprise UAC/sudo prompts).
- Health check polls 5 times × 2s after install — if the server still does not answer, returns
OLLAMA_NOT_RUNNINGrather than hanging. - The startup hook NEVER raises — any unhandled exception is logged and swallowed so the Langflow server keeps booting.
- The pipeline runs the four checks in order:
3.2 Domain Events (logical)
| Event | Trigger | Payload | Consumers |
|---|---|---|---|
local_model_bootstrap_starting |
Server lifespan begins, env flag not set | — | Operator (log) |
local_model_pull_progress |
Each new NDJSON status from /api/pull |
status (e.g. "downloading", "verifying sha256 digest") |
Operator (log), future progress UI |
local_model_bootstrap_ready |
Pipeline returned READY | — | Operator (log) |
local_model_bootstrap_finished |
Pipeline returned a non-READY terminal status | status, message |
Operator (log) |
local_model_bootstrap_failed |
Unhandled exception inside the hook | error_class, message |
Operator (log) |
4. Behavior Specifications
Feature: Langflow runs out-of-the-box without an API key
As a new Langflow user I want to open and run a starter project on first launch So that I can evaluate Langflow without creating any third-party account
Background
- Given Langflow is freshly installed
- And no
OPENAI_API_KEY(or any model provider credential) is set in the environment - And no model providers are configured in the user's database
Scenario: Langflow Model appears first in the unified catalog
- Given the backend has booted
- When the frontend calls
GET /api/v1/models - Then the response includes a provider entry with
provider="Langflow Model",models=[{"model_name": "qwen2.5:1.5b", "metadata": { "default": true, "tool_calling": true }}] - And Langflow Model is the first non-empty provider entry
Scenario: User selects Langflow Model in a starter project's Agent
- Given the user opens the "Basic Prompting" starter project
- When the project is loaded
- Then the
LanguageModelComponentnode hastemplate.model.value == "qwen2.5:1.5b"(set by the migration script) - And the dropdown's selected entry shows
qwen2.5:1.5bunder "Langflow Model"
Scenario: First-time pull happens automatically on flow execution
- Given Ollama is installed and running but the curated model is not yet pulled
- When the user clicks "Run" on a flow that uses the
Language Modelcomponent - Then
ChatLangflowLocal.__init__discovers the model is missing - And synchronously calls
POST /api/pullto downloadqwen2.5:1.5b - And once the pull completes, the actual invoke proceeds
Scenario: Server bootstrap pulls the model in the background
- Given the Langflow server starts and
LANGFLOW_DISABLE_LOCAL_MODEL_BOOTSTRAPis not set - When the FastAPI lifespan hook fires
- Then
schedule_local_model_bootstraprunsensure_local_model_readyin a background asyncio task - And server startup is not blocked by the pull
- And progress is logged every time the NDJSON status changes
Scenario: User declines installation
- Given Ollama is not installed
- And the bootstrap is invoked through the REST endpoint
- When the consent callback returns
False - Then no installer subprocess runs
- And the outcome is
BootstrapStatus.INSTALL_DECLINED
Scenario: User runs Langflow inside a container
- Given the container has
/.dockerenvpresent (orKUBERNETES_SERVICE_HOSTset) - When the bootstrap runs
- Then the pipeline short-circuits to
BootstrapStatus.DOCKER_GUIDANCE - And
DockerInstalleris selected by the factory (precedence oversystem_name) - And the consent callback is NEVER invoked
- And the user sees guidance to use
host.docker.internalor adocker-composesidecar
Scenario: User attempts SSRF via the language model component
- Given an attacker controls a flow's
base_urlfield - When they set it to
http://169.254.169.254/latest/meta-data/ - Then
ChatLangflowLocal.__init__raisesUnsafeBaseUrlError - And no HTTP request is made
- And the error message does not echo the malicious URL verbatim (log injection guard)
Scenario: User attempts to pull an unbounded model
- Given an attacker tries to instantiate
ChatLangflowLocal(model="llama3.1:405b") - When
__init__runs - Then
UncuratedModelErroris raised - And no Ollama HTTP call is made
Scenario: REST endpoint requires authentication
- Given a request to
GET /api/v1/local_model/statuswithout auth headers - When the server processes the request
- Then the response status is
401or403
Scenario: REST setup endpoint requires explicit consent body
- Given an authenticated user POSTs to
/api/v1/local_model/setup - When the body is
{"consent": false} - Then the response is
400 Bad Request - And no background task is scheduled
Scenario: Migration script is idempotent
- Given the migration script ran once and updated all starter projects
- When it is run a second time
- Then zero files are modified
Scenario: Frontend modal title does not render false
- Given the user opens the Model Providers modal and selects "Langflow Model"
- When the panel renders
- Then the title is
"Langflow Model"exactly — no trailingfalsetoken
Scenario: Frontend "Activate" works for credential-free providers
- Given the user is on the Langflow Model panel
- When they click "Activate Langflow Model"
- Then the system creates a global variable for
LANGFLOW_LOCAL_BASE_URLwith valuehttp://localhost:11434 - And the provider is marked
is_enabled=True - And no "Provider not supported" toast is displayed
5. Architecture Decision Records
ADR-001: Wrap Ollama instead of bundling llama-cpp-python
Status: Accepted
Context
Langflow needs a "no API key required" experience. Three credible options were evaluated:
- Bundle
llama-cpp-python+ a quantized GGUF model and serve it in-process. - Bundle GPT4All / a similar Python SDK with its own server.
- Wrap Ollama (already a first-class provider in Langflow) and curate one model on top.
Decision
Wrap Ollama. Register a new provider "Langflow Model" in the unified catalog whose backing class ChatLangflowLocal extends langchain-ollama.ChatOllama. The base URL points at localhost:11434 by default.
Consequences
Benefits:
- Reuses 100% of the existing Ollama integration: streaming, tool calling, embeddings, model dropdown plumbing, error handling.
- Zero new heavy Python dependencies (
llama-cpp-pythonships fragile native wheels per platform/CUDA/Metal combination). - Ollama has battle-tested cross-platform installers (
.pkg,.exe,install.sh) maintained by an active project. - Users who already have Ollama running get instant value.
Trade-offs:
- Adds a runtime dependency on Ollama as an external process (not embedded in the Python wheel).
- Auto-install requires explicit user consent and platform-specific code paths (mitigated by the Strategy pattern in
installers/).
Impact on Product:
- Time-to-first-flow drops from "create OpenAI account → get API key → set env var → restart" to "click Run". A
~1GBfirst-run pull is the only friction.
ADR-002: Curated single-model list, NOT dynamic discovery
Status: Accepted (revised after live test feedback)
Context
The first iteration registered "Langflow Model" in LIVE_MODEL_PROVIDERS, expecting it to inherit Ollama's dynamic-discovery behavior (read installed models from /api/tags at runtime). In production this caused the unified models endpoint to replace our curated 3-model list with an empty live list, because no fetch_live_langflow_local_models function existed in model_utils.py.
A second concern: showing the user 3 models (qwen2.5:1.5b, llama3.2:1b, llama3.2:3b) implies they are all interchangeable, but each requires a separate ~1GB download. This is bad UX for the zero-config promise.
Decision
Keep the Langflow Model list curated and static. Today the list contains exactly one model: qwen2.5:1.5b. It is not in LIVE_MODEL_PROVIDERS. Advanced users who want other models continue to use the Ollama provider directly.
Consequences
Benefits:
- Zero-config UX is honest: one provider, one model, one ~1GB download.
- The unified-models pipeline never tries to overwrite our entries with empty live data.
- The threat surface for "DoS by triggering a 200GB pull" is minimized — only one curated download path exists.
Trade-offs:
- Users wanting a different local model must use the Ollama provider explicitly. We document this in the provider description.
- Adding new curated models in the future requires a code change (deliberate — each new model adds disk-footprint and tool-calling-quality risk).
Impact on Product:
- New users see exactly one Langflow Model entry in the dropdown — no decision fatigue.
ADR-003: Strategy pattern for cross-platform installers
Status: Accepted
Context
Auto-installing Ollama is platform-specific and security-sensitive:
- Linux:
curl -fsSL https://ollama.com/install.sh | sh(sudo prompt) - macOS:
brew install ollamaif Homebrew is available; otherwise UNSUPPORTED with guidance - Windows: Download
OllamaSetup.exeand execute (UAC prompt) - Docker: Refuse, emit guidance for
host.docker.internal/ docker-compose sidecar
A naive if/elif/else chain on platform.system() mixes responsibilities and grows with every new path.
Decision
Define a structural Installer Protocol with a single install(consent_callback) -> InstallOutcome method. Implement one concrete strategy per platform plus a Docker refuser. A factory (get_installer()) inspects is_docker() first, then system_name(), and returns the correct strategy.
Consequences
Benefits:
- OCP: adding a future platform is a new file + one factory edit.
- Each strategy is independently testable with mocked
subprocess/httpx. - Test pinning:
test_docker_takes_precedence_over_system_namecodifies that a Linux container always getsDockerInstaller— protects against silent regression. - Clear seam for security review: each installer's threat model is documented in its own test file.
Trade-offs:
- Slightly more files than a single
installer.pywould have (5 strategies + 1 protocol + 1 factory). Justified by clean separation of concerns.
Impact on Product:
- Customers on every supported platform get a consistent, predictable install UX with explicit consent.
ADR-004: Auto-Pull On Demand at component instantiation
Status: Accepted (added in response to live test feedback)
Context
The startup hook (ADR-005) pulls the model in the background when the server boots. But two scenarios slip through:
- The user runs a flow before the background pull completes (~1GB takes time).
- The user runs Langflow on an environment where the startup hook is disabled or failed silently.
Both cases produce a confusing model 'qwen2.5:1.5b' not found (status code: 404) error.
Decision
Inside ChatLangflowLocal.__init__, before delegating to ChatOllama.__init__:
- Synchronously check
GET /api/tagsfor the model. - If not present, synchronously call
POST /api/pulland stream until completion. - If any step fails, swallow the error silently — the subsequent
ChatOllamainvoke will surface the original 404 to the user, preserving prior behavior for diagnostic purposes.
A new constructor flag auto_pull: bool = True allows tests and advanced callers to skip this.
Consequences
Benefits:
- The first flow execution Just Works™ even if the user did not wait for the background pull.
- No interface change for downstream callers (
Language Modelcomponent does not need updates). - Tests still pass because the helper swallows network errors when no Ollama is running locally.
Trade-offs:
- The first instantiation can block for several minutes while ~1GB is downloaded. Users see "thinking" with no progress bar (yet).
- Synchronous HTTP inside
__init__is unusual — duplicated from the async helpers inmodel_puller.pybecause component build paths are sync.
Impact on Product:
- The 404 error class disappears for new users — replaced by a long first-run experience that ends in success. Frontend progress UI is a follow-up.
ADR-005: Server-side bootstrap as a FastAPI lifespan background task
Status: Accepted
Context
The bootstrap orchestrator (ensure_local_model_ready) needs to run somewhere. Options:
- CLI on first startup: requires interactive prompts; breaks Docker / managed deployments.
- REST endpoint only (
POST /api/v1/local_model/setup): requires the user to know to call it. - FastAPI lifespan hook: starts automatically when the server boots, runs in the background, never blocks request handling.
Decision
Both (3) and (2). The lifespan hook (schedule_local_model_bootstrap) auto-runs on server start. The REST endpoint stays as a manual re-trigger / future UI hookpoint. An env var LANGFLOW_DISABLE_LOCAL_MODEL_BOOTSTRAP=true lets cloud / managed deployments opt out.
Consequences
Benefits:
- Self-hosted users get the model ready with zero CLI / UI interaction.
- Cloud operators retain a kill switch via env var.
- The hook runs in
asyncio.create_taskand stores the task onapp.state(RUF006 compliance) to avoid garbage-collection mid-flight. - The hook MUST NOT raise — wrapped in
try/except Exceptionso a Bootstrap bug never kills the server.
Trade-offs:
- On first boot, the user might see the model "appear" minutes after the server is up. The status endpoint exposes progress for any UI to poll.
Impact on Product:
- Provides a clear single-server-start UX path: log in, see the provider, run flows.
ADR-006: Marker variable to satisfy the existing "activate" UI
Status: Accepted (added after live UI test)
Context
The first design set MODEL_PROVIDER_METADATA["Langflow Model"]["variables"] = [] because the provider needs no credentials. The frontend's handleActivateProvider hook (in useProviderConfiguration.ts) requires at least one variable to attach the is_enabled flag to — empty variables triggered the toast Provider "Langflow Model" is not supported.
Decision
Attach exactly one non-secret, non-required variable to the provider:
{
"variable_name": "Langflow Model Base URL",
"variable_key": "LANGFLOW_LOCAL_BASE_URL",
"is_secret": False,
"required": False,
"component_metadata": { "advanced": True, "info": "Defaults to http://localhost:11434" },
}
This mirrors how Ollama uses OLLAMA_BASE_URL. It is hidden in the advanced section of the component UI.
Consequences
Benefits:
- Existing activate flow works without frontend rewrites.
- Test invariant tightened from "variables == []" to "no variable is secret AND no variable is required" — same zero-config promise, more accurate.
Trade-offs:
- The
Langflow Model Base URLfield is technically present in the modal. Mitigated byadvanced: True(hidden by default).
Impact on Product:
- Provider activation is a single click; no surprise input field surfaces unless the user opens "Advanced".
ADR-007: Frontend false-rendering bug fix scoped to this feature
Status: Accepted
Context
ProviderConfigurationForm.tsx:143 had a long-standing latent bug:
`${selectedProvider.provider} ${requiresConfiguration && ` ${t(...)}`}`
When requiresConfiguration === false, JS evaluates false && ... to the literal false, which gets concatenated as a string. Every existing provider had requiresConfiguration === true (they all need credentials), so the bug never surfaced — until Langflow Model arrived.
Decision
Replace the &&-as-conditional pattern with an explicit ternary in the same expression:
`${selectedProvider.provider}${requiresConfiguration ? ` ${t(...)}` : ""}`
Consequences
Benefits:
- Removes a latent bug that would surface for any future credential-free provider.
- One-line change; near-zero regression risk.
Trade-offs:
- None.
Impact on Product:
- Modal title reads "Langflow Model" cleanly instead of "Langflow Model false".
6. Technical Specification
6.1 Dependencies
| Type | Name | Purpose |
|---|---|---|
| Python package | langchain-ollama |
ChatOllama base class for ChatLangflowLocal |
| Python package | httpx |
Async + sync HTTP for health, version, tags, pull |
| Python package | fastapi |
REST endpoints + lifespan hook |
| External process | ollama |
Local LLM runtime (installed automatically with consent) |
| External service | https://ollama.com/install.sh |
Linux install script (URL-pinned) |
| External service | https://ollama.com/download/OllamaSetup.exe |
Windows installer (URL-pinned) |
| External service | https://ollama.com/download/Ollama-darwin.zip |
macOS fallback download (URL-pinned, currently UNSUPPORTED if no brew) |
| External package manager | Homebrew (macOS) | Preferred install path for brew install ollama |
| External tool | curl, sh (Linux) |
Run the official install script |
| Internal API | lfx.base.models.unified_models |
Provider registration |
6.2 API Contracts
GET /api/v1/local_model/status
Purpose: Return a non-mutating snapshot of the local-model bootstrap state.
Auth: Requires authenticated user (CurrentActiveUser dependency).
Request: no body.
Response (200):
{
"is_docker": false,
"is_ollama_installed": true,
"is_ollama_running": true,
"is_model_pulled": false,
"default_model": "qwen2.5:1.5b",
"ready": false
}
Response (401/403): emitted by the auth dependency when the request is unauthenticated.
POST /api/v1/local_model/setup
Purpose: Schedule ensure_local_model_ready to run in a background task.
Auth: Requires authenticated user.
Request:
{ "consent": true }
Response (202 Accepted):
{ "accepted": true }
Response (400 Bad Request): when consent is false or missing.
{ "detail": "Explicit consent required" }
6.3 Error Handling
| Error class | Condition | User Message | Recovery Action |
|---|---|---|---|
UnsafeBaseUrlError |
base_url not in ALLOWED_BASE_URLS |
"base_url is not in the Langflow Model allowlist" (no input echoed) | Use localhost, 127.0.0.1, or host.docker.internal. |
UncuratedModelError |
model not in CURATED_MODEL_NAMES |
"model is not in the Langflow Model curated set" | Use qwen2.5:1.5b, or use the Ollama provider for arbitrary models. |
LangchainOllamaMissingError |
langchain-ollama is uninstalled |
Includes "install" + package name | uv pip install langchain-ollama |
BootstrapStatus.DOCKER_GUIDANCE |
Running inside a container | "Run Ollama on the host (host.docker.internal) or add an ollama/ollama service to docker-compose" |
Use docker-compose sidecar or env var pointing at host. |
BootstrapStatus.OLLAMA_NOT_RUNNING |
Health check failed after install | "Start it with ollama serve (or open the Ollama app on macOS) and try again." |
Manually start Ollama. |
BootstrapStatus.PULL_FAILED |
NDJSON stream reported an error or ended without success terminator | Forwarded error field from Ollama |
Check disk space, network, and re-run setup. |
4xx from POST /api/v1/local_model/setup |
Missing or false consent |
"Explicit consent required" | Re-submit with {"consent": true}. |
7. Observability
7.1 Key Metrics (recommended for future dashboards)
| Metric | Type | Description | Alert Threshold |
|---|---|---|---|
local_model.bootstrap.duration_seconds |
Histogram | End-to-end bootstrap duration (start → READY) | P95 > 300s for 3 consecutive bootstraps |
local_model.pull.duration_seconds |
Histogram | NDJSON pull stream duration | P95 > 600s |
local_model.bootstrap.outcome |
Counter (label: status) |
Distribution of READY / DOCKER_GUIDANCE / INSTALL_DECLINED / INSTALL_FAILED / OLLAMA_NOT_RUNNING / PULL_FAILED / UNSUPPORTED_OS | pull_failed rate > 5% over 1h |
local_model.auto_pull.invocations |
Counter | How often the synchronous on-demand auto-pull fires | spike > 2x baseline |
local_model.auto_pull.failures |
Counter | Auto-pull errors caught and swallowed | rate > 10% of invocations |
7.2 Important Logs
| Level | Event | Fields | When |
|---|---|---|---|
| INFO | local_model_bootstrap_starting |
(none) | Lifespan hook fires |
| INFO | local_model_pull_progress |
status |
NDJSON status changes (e.g., "pulling manifest", "downloading", "verifying sha256 digest", "success") |
| INFO | local_model_bootstrap_ready |
(none) | Pipeline returned READY |
| INFO | local_model_bootstrap_finished |
status, message |
Pipeline returned a non-READY terminal status |
| INFO | local_model_bootstrap_skipped |
reason=env_flag |
LANGFLOW_DISABLE_LOCAL_MODEL_BOOTSTRAP is set |
| WARN | local_model_bootstrap_failed |
error_class, message |
Unhandled exception inside the hook |
No PII, secrets, or full URL strings are logged. Errors echo only the exception class name plus the bounded message.
7.3 Dashboards
Not yet provisioned — the metrics above are the recommended starter set when a dashboard is added.
8. Deployment & Rollback
8.1 Feature Flags
| Flag | Purpose | Default | Rollout Strategy |
|---|---|---|---|
LANGFLOW_DISABLE_LOCAL_MODEL_BOOTSTRAP |
Skip the server-side startup hook (cloud / managed deployments) | unset (= enabled) | Document in deployment guide; cloud product turns it ON. |
No database / runtime feature flag system is added. The single env var is sufficient for the rollback strategy.
8.2 Database Migrations
None. The feature uses the existing user-variables table to record:
- The marker variable
LANGFLOW_LOCAL_BASE_URL(created on first activation, valuehttp://localhost:11434). - Optional
__default_language_model__for global default selection (existing mechanism).
The 16 starter project JSON files are mutated in-place by an idempotent script:
python -m scripts.local_model.migrate_starter_projects
Rerunning the script is safe and reports Modified 0 starter project(s) after the first successful run.
8.3 Rollback Plan
- Disable the startup hook by setting
LANGFLOW_DISABLE_LOCAL_MODEL_BOOTSTRAP=trueand restarting the server. - Hide the provider from the catalog by reverting the entry in
MODEL_PROVIDER_METADATAand removingLANGFLOW_LOCAL_MODELS_DETAILEDfromprovider_queries.get_models_detailed(). No data needs migration. - Restore starter projects by running the script in reverse (or by
git checkoutof the JSON files — they are committed assets). - Frontend rollback: revert the icon mapping and the two new query hooks. The
LOCAL_MODELURL constant becomes dead code but harmless.
The user-database does not retain Langflow-Model-specific records that block rollback. The marker variable, if present, becomes a stale row but does not affect other providers.
8.4 Smoke Tests
After deploy:
GET /api/v1/local_model/statusreturns 200 with the expected JSON shape.GET /api/v1/modelsreturns "Langflow Model" as the first non-empty group withmodels=[{"model_name": "qwen2.5:1.5b", ...}].- Open Model Providers modal in the UI → "Langflow Model" appears with 1 model badge (not 0, not "false").
- Click Activate → success toast, no "Provider not supported" error.
- Open "Basic Prompting" starter project → the
Language Modelcomponent already hasqwen2.5:1.5bselected. - In server logs after restart: see
local_model_bootstrap_startingfollowed eventually bylocal_model_bootstrap_ready. - Run a flow end-to-end without
OPENAI_API_KEYset — it produces output.
9. Architecture Diagrams
9.1 Context Diagram (Level 1)
C4Context
title System Context — Langflow Local Model
Person(user, "Langflow User", "Wants to run flows without an API key")
System(langflow, "Langflow Server", "Runs flows; ships with the bundled local provider")
System_Ext(ollama, "Ollama", "Local LLM runtime (separate process on the user's host)")
System_Ext(ollama_cdn, "ollama.com", "Distributes installers and model weights")
Rel(user, langflow, "Opens UI, runs flows")
Rel(langflow, ollama, "HTTP /api/version, /api/tags, /api/pull, /api/chat")
Rel(langflow, ollama_cdn, "Downloads installer (consent required)")
Rel(ollama, ollama_cdn, "Pulls model weights")
9.2 Container Diagram (Level 2)
C4Container
title Containers — Langflow Local Model
Person(user, "User")
Container(frontend, "Langflow Frontend", "React", "Model Providers modal + Agent dropdown")
Container(backend, "Langflow Backend", "FastAPI / Python", "Hosts /api/v1/* endpoints + lifespan hook")
Container(lfx, "lfx Library", "Python", "Provider registry + bootstrap orchestrator + installers")
ContainerDb(db, "Langflow DB", "Postgres / SQLite", "Stores marker variable LANGFLOW_LOCAL_BASE_URL and is_enabled flag")
Container_Ext(ollama, "Ollama", "Native binary", "Local LLM server on localhost:11434")
Rel(user, frontend, "Browser")
Rel(frontend, backend, "GET /api/v1/local_model/status, POST /setup, GET /models")
Rel(backend, lfx, "ensure_local_model_ready, ChatLangflowLocal")
Rel(backend, db, "Reads/writes user variables")
Rel(lfx, ollama, "HTTP probes + chat invokes")
9.3 Component Diagram (Level 3) — Bootstrap Pipeline
flowchart TD
A[Server Lifespan Hook] -->|asyncio.create_task| B[schedule_local_model_bootstrap]
B --> C{ensure_local_model_ready}
C --> D{is_docker?}
D -->|yes| Z1[DOCKER_GUIDANCE]
D -->|no| E{is_ollama_installed?}
E -->|no| F[get_installer]
F --> G{install consent_callback}
G -->|DECLINED| Z2[INSTALL_DECLINED]
G -->|FAILED| Z3[INSTALL_FAILED]
G -->|UNSUPPORTED| Z4[UNSUPPORTED_OS]
G -->|SUCCESS / ALREADY_INSTALLED| H
E -->|yes| H
H[_wait_for_ollama_running 5x2s] -->|false| Z5[OLLAMA_NOT_RUNNING]
H -->|true| I{is_model_pulled}
I -->|true| Z6[READY]
I -->|false| J[pull_model NDJSON stream]
J -->|SUCCESS| Z6
J -->|FAILED / NETWORK_ERROR| Z7[PULL_FAILED]
10. Platform Compatibility
10.1 Supported Platforms
| Platform | Versions | Architecture | Status |
|---|---|---|---|
| Linux | Ubuntu 22.04+, Debian 12+, Fedora 39+ | x86_64, arm64 | Supported |
| macOS | 13 (Ventura)+ | x86_64, arm64 (Apple Silicon) | Supported (requires Homebrew for auto-install) |
| Windows | 10 22H2, 11 | x86_64 | Supported (UAC consent required) |
| Docker | linux/amd64, linux/arm64 | — | Supported via host.docker.internal or ollama/ollama sidecar; auto-install inside the container is intentionally refused |
10.2 Platform-Specific Implementations
| Capability | Linux | macOS | Windows | Docker | Notes |
|---|---|---|---|---|---|
| OS detection | Linux |
Darwin |
Windows |
n/a | system_name() returns linux/macos/windows/unknown. |
| Container detection | /.dockerenv + KUBERNETES_SERVICE_HOST |
same | same | same | is_docker() — multiple signals reduce false negatives. |
| Ollama binary lookup | shutil.which("ollama") + ollama --version (subprocess, list-args, timeout=3s) |
same | same (resolves .exe via PATHEXT) |
n/a | All paths share the same code; corroborated to defeat fake binaries. |
| Ollama install | sh -c "curl -fsSL https://ollama.com/install.sh | sh" |
brew install ollama (preferred); .zip fallback documented as UNSUPPORTED |
Download OllamaSetup.exe + subprocess.run([exe]) (UAC) |
Refused — DockerInstaller returns UNSUPPORTED with guidance |
Each in its own file under installers/. |
| Health check | httpx.AsyncClient.get("/api/version") with 2s timeout |
same | same | targets host.docker.internal:11434 |
SSRF-guarded against the ALLOWED_BASE_URLS frozenset. |
| Model pull | httpx.AsyncClient.stream("POST", "/api/pull") parsing NDJSON |
same | same | same | SUCCESS only via the {"status":"success"} terminator. |
| Auto-install consent prompt | sudo password prompt from the install script | macOS keychain prompt (Homebrew) or Gatekeeper | UAC prompt | n/a | Always explicit; consent_callback returning False aborts. |
10.3 Known Platform-Specific Limitations
- macOS without Homebrew:
MacOSInstallerreturnsUNSUPPORTEDwith a guidance message instead of attempting the.zipfallback. Downloading and moving an app bundle to/Applicationsprogrammatically is high-risk; we punt to documentation. Workaround:brew install ollamaor install Ollama manually fromhttps://ollama.com/download. - Windows silent install:
OllamaSetup.exedoes not expose a documented/Sflag. UAC is mandatory. - Docker auto-install: Refused by design. Inside a container the host filesystem, GPU passthrough, and systemd are not guaranteed. Use
host.docker.internal:11434or add aollama/ollamaservice indocker-compose.yml. - Apple Silicon Rosetta: Not required —
langchain-ollamaand Ollama natively support arm64. - Disk space: The default model is ~1GB after pull. Disk-full conditions surface as
BootstrapStatus.PULL_FAILEDwith the Ollama error forwarded. - Corporate proxies: Ollama's pull respects
HTTP_PROXY/HTTPS_PROXYenv vars;httpxin our code respects the same. If your proxy MITMs HTTPS, you must trust the corporate CA.
10.4 Installation by Platform
Linux (auto)
When Ollama is missing and consent is granted:
sh -c "curl -fsSL https://ollama.com/install.sh | sh"
Manual:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:1.5b
macOS (auto)
When Ollama is missing and Homebrew is present:
brew install ollama
ollama pull qwen2.5:1.5b
If Homebrew is absent, install Ollama manually from https://ollama.com/download and Langflow will pick it up on next start.
Windows (auto, PowerShell)
# Triggered by the WindowsInstaller after consent
Invoke-WebRequest "https://ollama.com/download/OllamaSetup.exe" -OutFile "$env:TEMP\OllamaSetup.exe"
& "$env:TEMP\OllamaSetup.exe" # UAC prompt
ollama pull qwen2.5:1.5b
Docker
Auto-install inside the container is refused. Two supported patterns:
Pattern A — Ollama on the host:
docker run -e LANGFLOW_LOCAL_MODEL_BASE_URL=http://host.docker.internal:11434 langflow/langflow
# Linux hosts may need: --add-host=host.docker.internal:host-gateway
Pattern B — Sidecar in docker-compose.yml:
services:
ollama:
image: ollama/ollama
volumes:
- ollama_data:/root/.ollama
ports:
- "11434:11434"
langflow:
image: langflow/langflow
environment:
- LANGFLOW_LOCAL_MODEL_BASE_URL=http://ollama:11434
depends_on:
- ollama
volumes:
ollama_data:
10.5 CI Coverage Matrix
| OS | Unit Tests | Integration | E2E | Smoke (Docker) |
|---|---|---|---|---|
| Ubuntu (latest) | ✅ | ✅ (mocks Ollama) | ➖ | ➖ |
| macOS (latest) | ✅ | ✅ (mocks Ollama) | ➖ | ➖ |
| Windows (latest) | ✅ | ✅ (mocks Ollama) | ➖ | ➖ |
| Docker (linux/amd64) | ➖ | ✅ (refusal path) | ➖ | ➖ |
➖ = intentionally not yet covered. E2E with a real Ollama is gated on a separate Playwright spec (follow-up). All current tests run with mocked subprocess and HTTP — no test depends on a real Ollama or a real network. This makes the suite fast (≈8 seconds for 165 lfx tests) and CI-portable.
Appendix A — File Inventory
Backend (lfx)
src/lfx/src/lfx/base/models/
langflow_local_constants.py # provider name, default model, allowlists
langflow_local_model.py # ChatLangflowLocal + 3 validators + auto-pull
model_metadata.py # +entry "Langflow Model"
unified_models/
provider_queries.py # +LANGFLOW_LOCAL_MODELS_DETAILED first
class_registry.py # +ChatLangflowLocal lazy import
src/lfx/src/lfx/services/local_model/
__init__.py
platform_detection.py # system_name, is_docker
ollama_binary.py # is_installed, binary_path
ollama_health.py # is_running, version (async)
model_puller.py # is_model_pulled, pull_model (NDJSON)
installer_factory.py # get_installer (strategy selector)
installers/
__init__.py
protocol.py # Installer Protocol + InstallOutcome + InstallStatus
linux.py # curl|sh
macos.py # brew install
windows.py # download + UAC
docker.py # refuse + guidance
bootstrap.py # ensure_local_model_ready
Backend (langflow)
src/backend/base/langflow/api/v1/
local_model.py # GET /status + POST /setup
__init__.py # +export local_model_router
router.py # +include_router
src/backend/base/langflow/services/local_model/
__init__.py
startup_hook.py # schedule_local_model_bootstrap
src/backend/base/langflow/main.py # +lifespan task
Frontend
src/frontend/src/controllers/API/queries/local-model/
use-get-local-model-status.ts # React Query hook
use-setup-local-model.ts # React Query mutation
__tests__/ # Jest specs
src/frontend/src/controllers/API/queries/models/
use-get-model-providers.ts # +"Langflow Model": "Langflow" icon mapping
src/frontend/src/controllers/API/helpers/constants.ts # +LOCAL_MODEL: "local_model"
src/frontend/src/modals/modelProviderModal/components/
ProviderConfigurationForm.tsx # bug fix for "false" rendering
Scripts
scripts/local_model/
__init__.py
migrate_starter_projects.py # idempotent JSON patcher
tests/test_migrate_starter_projects.py
Starter Projects
16 JSON files under src/backend/base/langflow/initial_setup/starter_projects/ were updated in-place by the migration script. Every LanguageModelComponent node now has template.model.value == "qwen2.5:1.5b".
Configuration
pyproject.toml # +ruff per-file-ignores for scripts/local_model/tests/*
Appendix B — Test Inventory
| Suite | Count | Coverage |
|---|---|---|
lfx/tests/unit/base/models/test_langflow_local_constants.py |
14 | 100% |
lfx/tests/unit/base/models/test_langflow_local_model.py |
22 | 100% |
lfx/tests/unit/base/models/test_langflow_local_provider_registration.py |
10 | 100% |
lfx/tests/unit/services/local_model/test_platform_detection.py |
9 | 100% |
lfx/tests/unit/services/local_model/test_ollama_binary.py |
9 | 100% |
lfx/tests/unit/services/local_model/test_ollama_health.py |
14 | 89% |
lfx/tests/unit/services/local_model/test_model_puller.py |
18 | 87% |
lfx/tests/unit/services/local_model/test_bootstrap.py |
17 | 96% |
lfx/tests/unit/services/local_model/installers/test_protocol.py |
10 | 95% |
lfx/tests/unit/services/local_model/installers/test_linux_installer.py |
9 | 100% |
lfx/tests/unit/services/local_model/installers/test_macos_installer.py |
7 | 100% |
lfx/tests/unit/services/local_model/installers/test_windows_installer.py |
5 | 95% |
lfx/tests/unit/services/local_model/installers/test_docker_installer.py |
3 | 100% |
lfx/tests/unit/services/local_model/test_installer_factory.py |
7 | 100% |
backend/tests/unit/test_local_model_api.py |
6 | endpoint contract |
frontend Jest (local-model + icon mapping) |
6 | hooks contract |
scripts/local_model/tests/test_migrate_starter_projects.py |
7 | idempotency |
| Total | 173 | avg 94% |
All 173 tests pass with 100% mocked external dependencies. No test requires a running Ollama server, network access, or real subprocess execution.