Skip to content

docs(model-provider): record the cross-provider model substitution as LE-2156 (#1372) - #1374

Merged
rafaelgiln merged 1 commit into
mainfrom
docs/issue-1372-le2156-cross-provider-fill
Aug 7, 2026
Merged

docs(model-provider): record the cross-provider model substitution as LE-2156 (#1372)#1374
rafaelgiln merged 1 commit into
mainfrom
docs/issue-1372-le2156-cross-provider-fill

Conversation

@rafaelgiln

Copy link
Copy Markdown
Collaborator

Refs #1372deliberately not Closes: that issue stays open per its own deliverable, pending the trigger and the upstream fix landing in the nightly.

Upstream ticket: LE-2156

What this PR is

The record for a confirmed langflow-regression, not a fix. It lands the evidence file, the mandatory ledger row, and a warning aimed squarely at the next reader. It changes no test, on purpose — see the last section.

What was established

POST /api/v2/workflows 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 Playground run confirms the frontend always sends it:

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",...}]

So the backend builds the canvas, not the row. GET /api/v1/flows/{id} is not a weak observable for the executed model — it is the wrong object.

The runtime does not substitute either: get_llm raises "A model selection is required" on an empty list. What substitutes is the editor build config, where an empty ModelInput value is filled with options[0] — and options is a flat list across every enabled provider, so the fill need not be the node's own provider.

Measured through POST /api/v1/custom_component/update (the endpoint the editor itself uses) on 1.12.0.dev19:

field_value sent returned model.value
[{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
[] claude-opus-5 / Anthropic replaced

Why the severity is High despite a low observed rate

It fails loudly in openai-compatible-provider-setup.spec.ts only because that provider's endpoint-derived default set starts with completions-only ids (404 … This is not a chat model). On 6 of the 8 providers measured — Anthropic, Google, OpenAI, Azure AI Foundry, Ollama, OpenRouter — options[0] is a working chat model, so the same substitution produces a passing run against a model nobody selected, on a provider nobody selected, billed to that provider's account.

That judgement is mine and is worth challenging: the trigger is unknown, so the frequency of the silent form is unmeasured. Say the word and it becomes Medium.

What was refuted — the load-bearing half

Two backend explanations were tested and both died, which is what narrows the trigger to editor state and spares upstream the same investigation:

  • an invalid selection is not 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;
  • an empty options list does not wipe a real selection — with every provider credential deleted, options comes back as 1, not 0, because the selection is injected into the options.

What empties the field on a node that had a selection is still unknown, and the report says so rather than implying a complete chain.

Honesty about the rate

The pre-fix baseline over 12 runs of the untouched spec reproduced the symptom zero times. The runner reported "3/12 failed (25 %)", but two of those carry unexpected=0 — no test failed, they were counted because a backend error was logged — and the single genuine failure carries TimeoutError: page.waitForResponse: Timeout 60000ms exceeded, a different symptom. The finding rests on the deterministic experiment above, not on frequency.

Why no spec changed

Strengthening the existing persisted-binding assertion would be engineering a spec to pass against an observable that structurally cannot see the defect. A re-selection repair loop was already written, measured against two real occurrences, and removed for exactly that reason (#1369).

The spec doc now states that the pre-send re-read is attribution and must not grow into a gate, so the next reader does not spend a cycle re-strengthening the wrong object. A guard that genuinely covers this belongs on the run request payload; it has wide reach across every spec that pins a model and is tracked separately rather than riding along here.

Validation

Langflow 1.12.0.dev19 (nightly). Docs-and-ledger change:

no .spec.ts in diff — docs-only change, burst skipped
typecheck=0  lint=0
REGRESSIONS.md indicator is in sync with the table (9 regression(s))

Ledger indicator after the row: 9 regressions — Open 3 · Fixed 6, High 3, model-provider 3.

Scout residue: every throwaway spec deleted, its leaked flow removed, and the three provider credentials the second experiment deleted were restored with the instance verified back to Anthropic / Google / OpenAI configured.

🤖 Generated with Claude Code

… LE-2156 (#1372)

A flow run can execute a model from a different provider than the node selects,
and neither the database nor the widget predicts it. This lands the evidence,
the ledger row and the warning for the next reader. It changes no test, on
purpose — see the last section.

## What was established

`POST /api/v2/workflows` 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 Playground run
confirms the frontend always sends it. So the backend builds the canvas, not the
row, and `GET /api/v1/flows/{id}` is not a weak observable for the executed
model — it is the wrong object.

The runtime does not substitute either: `get_llm` raises "A model selection is
required" on an empty list and "The selected model is missing a provider" on a
blank provider. What substitutes is the editor build config, where an EMPTY
`ModelInput` value is filled with `options[0]` — and `options` is a FLAT list
across every enabled provider, so the fill need not be the node's own provider.
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.

It fails loudly in `openai-compatible-provider-setup.spec.ts` only because that
provider's endpoint-derived default set starts with completions-only ids
(`404 ... This is not a chat model`). On 6 of the 8 providers measured,
`options[0]` is a working chat model, so the same substitution produces a
PASSING run against a model nobody selected, on a provider nobody selected,
billed to that provider's account.

## What was refuted, and why that is the load-bearing half

Two backend explanations were tested and both died, which is what narrows the
trigger to editor state and saves upstream the same investigation:

- an INVALID selection is not 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;
- an empty `options` list does not wipe a real selection — with every provider
  credential deleted, `options` comes back as 1, not 0, because the selection is
  injected into the options.

What EMPTIES the field on a node that had a selection is still unknown, and the
report says so rather than implying a complete chain.

## Honesty about the rate

The pre-fix baseline over 12 runs of the untouched spec reproduced the symptom
ZERO times. The runner reported "3/12 failed (25%)", but two of those carry
`unexpected=0` — no test failed, they were counted because a backend error was
logged — and the single genuine failure carries `TimeoutError:
page.waitForResponse: Timeout 60000ms exceeded`, a different symptom. The
finding rests on the deterministic experiment, not on frequency.

## Why no spec changed

Strengthening the existing persisted-binding assertion would be engineering a
spec to pass against an observable that structurally cannot see the defect. A
re-selection repair loop was already written, measured against two real
occurrences and removed for exactly that reason (#1369). The spec doc now states
that the pre-send re-read is attribution and must NOT grow into a gate, so the
next reader does not spend a cycle re-strengthening the wrong object; a guard
that genuinely covers this belongs on the run request and is tracked separately.

Refs #1372 — deliberately NOT "Closes": the issue stays open per its own
deliverable, pending the trigger and the upstream fix landing in the nightly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rafaelgiln rafaelgiln added follow-up Approved exception: follow-up of merged work (ROADMAP Intake) area:model-providers Provedores de modelos labels Aug 7, 2026
@rafaelgiln

Copy link
Copy Markdown
Collaborator Author

Why this PR says Refs and not Closes

The deterministic pipeline's PR gate refused this PR for a missing Closes #1372 line, and the refusal is being recorded rather than worked around.

#1372 tracks a confirmed langflow-regression (LE-2156) whose trigger is still unknown and whose fix has not landed in the nightly. Its own deliverable 2 says the issue stays open until it does. This PR delivers the record — the upstream evidence file, the mandatory REGRESSIONS.md ledger row, and a spec-doc section warning the next reader off the wrong observable — not the fix.

Writing Closes #1372 to satisfy the gate would close an issue that must stay open, and hand-editing the pipeline state is forbidden. The pipeline was therefore aborted with that reason on the record, and this PR stands on its own merit.

The gate encodes a premise that holds for almost every PR — a PR closes its issue — and does not hold for a record-only PR on a still-open product defect. Worth relaxing to accept Refs #NNN in that case; not changed here, because it lives under .claude/skills/ and would pollute this branch.

@rafaelgiln
rafaelgiln merged commit a08add5 into main Aug 7, 2026
7 checks passed
@rafaelgiln
rafaelgiln deleted the docs/issue-1372-le2156-cross-provider-fill branch August 7, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:model-providers Provedores de modelos follow-up Approved exception: follow-up of merged work (ROADMAP Intake)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant