Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions REGRESSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,18 @@ issue that carries the evidence. Both shapes are allowed; what is not allowed is
a reference that does not resolve.

<!-- REGRESSIONS:START -->
**Regressions caught:** 8 — **Open:** 2 · **Fixed:** 6
**Regressions caught:** 9 — **Open:** 3 · **Fixed:** 6

**By severity:** High 2 · Medium 6 · Low 0
**By severity:** High 3 · Medium 6 · Low 0

**By area:** model-provider 2 · api 1 · auth 1 · core-components 1 · flows 1 · mcp 1 · ui-ux 1
**By area:** model-provider 3 · api 1 · auth 1 · core-components 1 · flows 1 · mcp 1 · ui-ux 1
<!-- REGRESSIONS:END -->

## Ledger

| Found | Area / Test | Regression | Severity | Detected by | Upstream | Status | Fixed in | Report |
|-------|-------------|------------|----------|-------------|----------|--------|----------|--------|
| 2026-08-07 | model-provider · openai-compatible-provider-setup.spec.ts | A flow run can execute a model from a **different provider than the node selects**. An empty `ModelInput` value is filled by `update_build_config` with `options[0]`, and `options` is a **flat list across every enabled provider** (`get_language_model_options`), so the fill is the first default-enabled model of the first *configured* provider — not of the node's own. `POST /api/v2/workflows` then carries the live-canvas `data`, declared in `WorkflowRunRequest` as taking **priority over the saved flow data**, so the fill is what runs while the persisted flow still holds the correct selection. Measured through `POST /api/v1/custom_component/update` on 1.12.0.dev19: an OpenAI-Compatible node with an empty value came back `claude-opus-5` / **Anthropic**. Loud here only because that provider's endpoint-derived default set starts with completions-only ids (`404 … not a chat model`); `options[0]` is a working chat model for Anthropic, Google, OpenAI, Azure AI Foundry, Ollama and OpenRouter — **6 of 8** — where the same substitution runs green on a provider nobody selected. Two backend explanations were tested and **refuted** (an invalid selection is preserved; an empty `options` list does not wipe it, because the selection is injected into the options), and the run path is exonerated (`get_llm` raises rather than defaulting), so the trigger that empties the field is editor state and remains open | High | #1334 spec validation (PR #1369) · #1372 | [LE-2156](https://datastax.jira.com/browse/LE-2156) | Open | — | docs/upstream-bugs/UPSTREAM-BUG-model-input-cross-provider-default-fill.md |
| 2026-08-04 | ui-ux · global-variable-edit.spec.ts | Global Variables grid silently drops row interactions while the RBAC permission query (`POST /api/v1/authz/me/permissions`) is loading — a row click never opens the Update Variable modal and checkbox ticking selects nothing, so the delete button stays disabled, with no spinner or feedback of any kind. Normally a 1–2 s window, but one transient network/5xx failure on the first call holds the gate through the query wrapper's 5× exponential-backoff retry ladder: ~31 s of dropped input, measured to the decimal (retries at 0.8/1.8/3.8/7.8/15.8/31.8 s under an injected 503). Introduced by the release-1.12 RBAC gate (langflow#14215, `2e677bf843`). The gate blocks checkbox selection too (proven by the 503 injection), but this row rests on `global-variable-edit.spec.ts` alone: the recorded flake of `remove-provider-api-key.spec.ts:17` is NOT evidence of it — the same signature reproduces at ~75 % on a healthy 1.12.0 with the gate open, from a test defect (#1235) | Medium | daily 07-27 + 08-03 · #1231 → #1235 | [LE-2123](https://datastax.jira.com/browse/LE-2123) | Open | — | docs/upstream-bugs/UPSTREAM-BUG-global-variables-permission-gate-dead-window.md |
| 2026-07-28 | core-components · nested-grouping-regression.spec.ts | Grouping two connected non-IO components raises a false `Error while updating the Component` notification although the grouping fully succeeds — the `PATCH /api/v1/flows/{id}` that persists the grouped shape returns `200`, the console logs no error and no request fails, yet the message persists in the Notifications panel until dismissed by hand. Deterministic; reproduced independently in a manual browser session | Medium | #942 spec validation | [LE-2045](https://datastax.jira.com/browse/LE-2045) | Fixed | [langflow#14314](https://github.com/langflow-ai/langflow/pull/14314) | docs/upstream-bugs/UPSTREAM-BUG-group-cosmetic-error-toast.md |
| 2026-07-27 | api · api-folders-crud.spec.ts | `DELETE /api/v1/projects/{id}` answers `500` (`sqlite3.OperationalError: database is locked`) instead of `204` while any other write is in flight, and the project survives. Not new — stable 1.10.3 emits the same instant `500` — but 1.12 raises the rate ~7× (6 % → 44 % at 2 concurrent clients, A/B/A/B) and flips the mode: 1.10.3 blocks and mostly honours the contract, 1.12 gives up in 0.03 s. Sibling write endpoints (`POST /projects`, `POST /flows`, `DELETE /flows`) survive the identical contention | Medium | daily 07-22 + 07-27 · #962 → #965 | [LE-2020](https://datastax.jira.com/browse/LE-2020) | Open | — | docs/upstream-bugs/UPSTREAM-BUG-project-delete-500-under-contention.md |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,38 @@ it is how a spec passes on one instance and dies on a clean one. Test 5 therefor
`POST /api/v1/models/enabled_models` — the endpoint the **Azure AI Foundry sibling in this
same folder already uses** — and disables it again in cleanup.

### The persisted binding cannot predict the executed model — do not strengthen it (#1372, LE-2156)

Test 5's pre-send re-read of `GET /api/v1/flows/{id}` is **attribution, never repair**, and
the reason is structural rather than a matter of tuning. `POST /api/v2/workflows` — the run
the Playground issues — carries a `data` field that `WorkflowRunRequest` declares as an
*"Optional live-canvas override of the flow's nodes/edges; **takes priority over the saved
flow data**"*, and a capture of a **healthy** run confirms the frontend always sends it.
The backend therefore builds the canvas, not the row. `GET /api/v1/flows/{id}` is not a
weak observable here; it is the **wrong object**, and no amount of polling it harder can
close the gap.

Measured consequence: on 2 of 12 full-file runs the run answered
`404 … This is not a chat model` while the persisted read at the last influenceable
instant — after the Playground modal opened, immediately before `button-send` — returned
`{models: ["gpt-4o-mini"], providers: ["OpenAI Compatible"]}` and the widget agreed. A
re-selection repair loop was written, measured against exactly that, and **removed**:
re-selecting cannot fix a state that is already correct.

The mechanism is upstream (`LE-2156`, full report in
`docs/upstream-bugs/UPSTREAM-BUG-model-input-cross-provider-default-fill.md`): an **empty**
`ModelInput` value is filled with `options[0]`, and `options` is a **flat list across every
enabled provider**, so the fill need not even be this provider — an OpenAI-Compatible node
came back `claude-opus-5` / **Anthropic**. It fails loudly here only because this
provider's endpoint-derived default set starts with completions-only ids; on 6 of the 8
providers measured, `options[0]` is a working chat model and the same substitution runs
**green** against a model nobody selected.

So the only observable that predicts the executed model is the `data` payload of the run
request. Two things follow for anyone editing this spec: the pre-send read stays as an
attribution line and must not grow into a gate that pretends to prevent this, and a future
guard that genuinely covers it belongs on the run request, not on the flow row.

---

## Tags *(required)*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# A flow run can execute a model from a different provider than the node selects

**Upstream:** [LE-2156](https://datastax.jira.com/browse/LE-2156)
**Found:** 2026-08-07 · **Measured on:** `1.12.0.dev19` (`langflowai/langflow-nightly`)
**Detected by:** `#1334` spec validation (PR #1369) → dedicated issue `#1372`
**Status:** Open

---

## Summary

When a `ModelInput` field's value is empty, `update_build_config` fills it with
`options[0]`. `options` is a **flat list across every enabled provider**, so the
model filled in is the first default-enabled model of the **first configured
provider** — frequently not the provider the node is configured for.

Because `POST /api/v2/workflows` carries the live-canvas `data` override, which
by its own schema *"takes priority over the saved flow data"*, whatever the
editor filled in is what actually runs. The persisted flow can still hold the
correct selection at that moment.

**Net effect:** a flow can execute a model the persisted flow does not name,
from a provider the user never selected, with no error.

## How it surfaced here

`openai-compatible-provider-setup.spec.ts` failed **2 in 12** full-file runs
with:

```text
Error code: 404 - {'error': {'message': 'This is not a chat model and thus not
supported in the v1/chat/completions endpoint. Did you mean to use
v1/completions?', 'type': 'invalid_request_error', 'param': 'model'}}
```

`gpt-4o-mini` **is** a chat model. The only ids that provider offers which
produce that error are its completions-only ones (`babbage-002` / `davinci-002`)
— the first entries of its default-enabled set.

## Reproduction (deterministic)

Against `POST /api/v1/custom_component/update` — the endpoint the editor itself
uses — on an instance with Anthropic, Google Generative AI and OpenAI
configured, with a `LanguageModelComponent` node and `field: "model"`:

| `field_value` sent | returned `model.value` | verdict |
|---|---|---|
| `[{name: "gpt-4o-mini", provider: "OpenAI Compatible"}]` | unchanged | preserved |
| `[{name: "definitely-not-a-model", provider: "Nope"}]` | unchanged | preserved |
| `[{name: "gpt-4o-mini", provider: "OpenAI"}]` | unchanged | preserved |
| `[]` | `[{name: "claude-opus-5", provider: "Anthropic", …}]` | **replaced** |

The last row is the defect: the node's own provider is irrelevant to what it is
filled with. `options[0]` is Anthropic's first default-enabled model purely
because Anthropic comes first in the enabled-provider iteration order.

## Why it is usually silent — the reason this matters beyond one 404

The OpenAI-Compatible case fails **loudly** only because that provider's
default-enabled set is derived from the endpoint's own `GET <base_url>/v1/models`
and starts with completions-only ids. Measured on `1.12.0.dev19`, every other
provider's `options[0]` is a working chat model:

| Provider | `options[0]` |
|---|---|
| Anthropic | `claude-opus-5` |
| Google Generative AI | `gemini-3.5-flash-lite` |
| OpenAI | `gpt-5.6-sol` |
| Azure AI Foundry | `gpt-4o` |
| Ollama | `llama3.3` |
| OpenRouter | `anthropic/claude-opus-4.7` |

**6 of 8.** In those cases the substitution produces a *passing* run against a
model nobody selected, on a provider nobody selected, billed to that provider's
account — the #1169 silent-substitution class, one layer deeper.

## Source

`lfx/base/models/unified_models/build_config.py`:

```python
build_config[model_field_name]["value"] = (
field_value if value_is_valid else [options[0]] if options else "")
```

`lfx/base/models/unified_models/model_catalog.py` — `get_language_model_options`
accumulates **one flat list** across providers:

```python
for provider_data in all_models:
if provider not in enabled_providers:
continue
for model_data in models:
... # appended to the same `options`
```

## What is NOT the cause — refuted by experiment, not by argument

What *empties* the field on a node that had a selection is **not established**.
Two candidate backend paths were tested and both died:

1. **Not an invalid-selection reset.** A deliberately impossible model
(`definitely-not-a-model` / `Nope`) is **preserved**, so the `[options[0]]`
branch does not fire for out-of-catalog values.
2. **Not an empty `options` list wiping the value.** With every provider
credential deleted, `options` comes back as **1**, not 0 — the current
selection is injected into the options — and the value is **preserved**.

Both are backend paths, so the trigger looks like **editor/frontend state** that
never round-trips through `update_build_config`.

The **run path is not responsible** either: `get_llm`
(`lfx/base/models/unified_models/instantiation.py`) raises
`"A model selection is required"` on an empty list and `"The selected model is
missing a provider"` on a blank provider. It never falls back to a default.

## The observable, and why the obvious one cannot work

`POST /api/v2/workflows` sends the canvas. Captured on a **healthy** run — no
flake needed:

```text
keys = flow_id, input_value, mode, stream_protocol, session_id, start_component_id, data
data = PRESENT data.nodes = 6
node LanguageModelComponent-FLeYF
model.value = [{"name":"claude-opus-5","provider":"Anthropic",...,"default":true}]
```

`lfx/schema/workflow.py`, `WorkflowRunRequest`:

> `data` — *"Optional live-canvas override of the flow's nodes/edges; takes
> priority over the saved flow data."*

So `GET /api/v1/flows/{id}` is **structurally incapable** of predicting the
executed model. That is not a weak observable — it is the wrong object, and no
amount of strengthening the persisted-binding poll could have caught this. The
only observable that predicts the run is the `data` payload of the run request.

This is why `openai-compatible-provider-setup.spec.ts` keeps its pre-send
re-read as **attribution, not repair** (#1369): a re-selection loop was written,
measured against this, and removed — re-selecting cannot fix a state that is
already correct.

## Honesty about the rate

The pre-fix baseline over 12 runs of the untouched spec reproduced the 404
**zero** times. The runner reported *"3/12 failed (25 %)"*, but two of those
three carry `unexpected=0` — no test failed at all, they were classified as
failures because a backend error was logged — and the single genuine failure
carries `TimeoutError: page.waitForResponse: Timeout 60000ms exceeded`, a
different symptom.

The finding therefore rests on the deterministic experiment above, not on
frequency.

## Suggested fix direction

The fill is reasonable on a genuinely new node; crossing providers is not.
Scoping the default to the node's current provider — or refusing to fill when
the node already carries a provider identity — turns a silent cross-provider
substitution into either the right model or a visible error.