fix(model-provider): assert the provider, not a credential name upstream stopped writing (#1334) - #1369
Conversation
…eam stopped writing (#1334) Upstream #14311 ("stop automatic provider field binding", on the 1.12 line since 2026-08-04) deleted the block that pre-populated a provider field's value with the credential VARIABLE NAME, so `template.api_key.value` is now "" on every read from mount onward. This spec asserted that removed binding inline, so its poll could never settle: `models` correct, `credential: ""` vs "OPENAI_COMPATIBLE_API_KEY", 2/2 attempts on daily run 31093877484. The product hypothesis was refuted before the upstream lead was adopted. Differential on the same node, instance and session on 1.12.0.dev18/19: selecting a plain OpenAI model persists `api_key: {value: "", load_from_db: false}` exactly as the OpenAI Compatible selection does, so the empty value is build-wide, not a persistence failure on this provider. The backend agrees in source: `apply_provider_variable_config_to_build_config` is documented "Apply the current provider's metadata without changing explicit field values" and writes only show/required/advanced/info. `models` persists while `credential` does not because they have different writers — the selection is carried by the editor autosave, `api_key.value` was written only by the deleted block. The credential is no longer stored but is still DETERMINED, by the provider of the selected model, so the assertion moves onto that axis — the migration #1274 applied to the shared helper `agent-credential-settle.ts`, which this spec never received because it asserted the shape inline. Proved causally: `instantiation.py` reads `model.value[0].provider` and calls `get_api_key_for_provider`, which with an empty api_key resolves `get_provider_secret_variable_key(provider)`; dropping ONLY OPENAI_COMPATIBLE_API_KEY while a valid OPENAI_API_KEY stays configured account-wide leaves the binding unchanged and turns the run into "401 Incorrect API key provided: EMPTY". `api_key` is now read and printed in the diagnostic, asserted in neither direction (#1274's rule). Also, each measured while validating this: - Test 5 enables its model itself. `gpt-4o-mini` is `default: false` in this provider's live catalog and the node's dropdown offers default-enabled or explicitly enabled ids only, so on a clean instance the option never renders (2/2 click timeouts). It passed in CI only on model-status left by other specs — ambient state the spec never declared. Enabled via POST /api/v1/models/enabled_models (the Foundry sibling's call), disabled again in cleanup, with the catalog and the enabled map both polled first. - Test 4 polls the catalog's TERMINAL shape. It polled `num_models > 0` and then re-read, catching the catalog mid-registration: 124 instead of 248 twice, once an empty llm list, while the same endpoint seconds later reads {llm: 124, embeddings: 124}. - Test 6's quarantine is lifted. LE-2124's concurrent variable-write race answers 201/201 3/3 on 1.12.0.dev19 and the test passes 3/3 through the UI with its assertions untouched. Its rejection bodies are now captured, because an unreachable endpoint leaves the identical end state as the race: "Invalid OpenAI-compatible base URL" fails, a transport rejection skips with the body. - Environment aborts skip instead of reddening. The backend validates the key by calling the endpoint, so a stalled network answers 400 "timed out"; that shape is retried once then skips. A drained account is read off the run's own message ("no credits remaining" / "insufficient_quota"), invisible to the probe because GET /v1/models answers 200 for a key with no credit; `rate_limit_exceeded` still fails. Restores @stable on test 5, auto-removed by cb3082d. Closes #1334 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI verdict:
|
| Check | Result |
|---|---|
| Langflow version | 1.12.0.dev19 — same image as this lane (Langflow Nightly 1.12.0.dev19 appears in this job's own preflight) |
Deterministic burst, --workers=1 --retries=0 |
3 clean runs |
| Final green after the force-fail reverts | expected=6, unexpected=0, skipped=0 |
| Further full-file runs on the final code | 6/6 repeatedly |
| Force-fail | all 6 tests verified red under a marked mutation, then reverted; no // FF-MUTATION left in the diff |
typecheck / lint / validate:specs / both QA-CHECKLIST guards |
exit 0 (and all six non-E2E checks are green on this PR) |
Why merging is still the right call. The red belongs to a shared pre-flight that is currently failing for other PRs too and that this diff cannot influence; the change itself is validated on the target build, and holding it parks a fix for a @stable spec that is red in the daily behind an unrelated infra recurrence. The #1355 recurrence is worth its own issue — the 180 s wait #1357 introduced is not sufficient on a loaded runner — and I will file it separately rather than fold it in here.
If a reviewer wants CI-side proof of this spec before merging, dispatch manual.yml with test_grep on the changed file once Collect models is healthy.
… LE-2156 (#1372) (#1374) 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: Rafael <rafael@oriontech.me> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #1334
Root cause —
product-changed, not a regression and not a test defectUpstream #14311 ("stop automatic provider field binding",
646bdd6b, forward-ported torelease-1.12.0on 2026-08-04) deleted the block that pre-populated a provider field'svaluewith the credential variable name.template.api_key.valueis now""on every read, from mount onward. This spec asserted that removed binding inline, so its poll could never settle —modelshalf correct,credential: ""against an expected"OPENAI_COMPATIBLE_API_KEY", on 2/2 attempts of run 31093877484.The product hypothesis was refuted before the upstream lead was adopted, as the issue required. Differential on the same node, instance and session:
template.model.valuetemplate.api_keyOpenAI Compatible/gpt-3.5-turbo(×4)[{name, provider: "OpenAI Compatible", …}]{value: "", load_from_db: false, show: true, required: false}OpenAI/gpt-5.6[{name, provider: "OpenAI", …}]{value: "", load_from_db: false, …}An empty
api_keyis build-wide and provider-agnostic, not an OpenAI-Compatible persistence failure. The backend agrees in source:apply_provider_variable_config_to_build_configis documented "Apply the current provider's metadata without changing explicit field values" and writes onlyshow/required/advanced/info.modelspersisting whilecredentialdoes not has one explanation under this reading — different writers: the selection is carried by the editor's autosave,api_key.valuewas written only by the deleted block, and it already reads""before any selection is made.The fix — same migration #1274 applied to the shared helper
The credential is no longer stored but is still determined, by the provider of the selected model, so the assertion moves onto that axis.
tests/helpers/flows/agent-credential-settle.tsgot this in #1274; this spec never did, because it asserted the shape inline — it was the last such copy under that spelling.Proved causally, not assumed.
instantiation.pytakesprovider = model.get("provider")off the selected entry and callsget_api_key_for_provider, which with an emptyapi_keyresolvesget_provider_secret_variable_key(provider). With the pair configured the run answers and the message carriesproperties.source: "gpt-3.5-turbo"; deleting onlyOPENAI_COMPATIBLE_API_KEYwhile a validOPENAI_API_KEYstays configured account-wide leaves the persisted binding unchanged and turns the same run into401 - Incorrect API key provided: EMPTY. The run resolves this provider's key and does not borrow the OpenAI provider's — which is what the old assertion existed to protect, and the provider axis is stronger here, becauseapi.openai.comserves ids the plain OpenAI provider also serves.api_keyis now read and printed in the poll's diagnostic and asserted in neither direction (#1274's rule — requiring it empty would swap one dated premise for another and break amanual.ymldispatch at a pre-#14311 build).Also in this PR — each found and measured while validating the above
gpt-4o-miniisdefault: falsein this provider's live catalog, and the node's dropdown offers default-enabled or explicitly enabled ids only (model_catalog.py), so on a clean instanceOpenAI Compatible-gpt-4o-mini-optionnever renders — 2/2 click timeouts, well before the binding assertion. It passed in CI only on model-status left by other specs: ambient state this spec never declared. Now enabled viaPOST /api/v1/models/enabled_models(the Azure AI Foundry sibling's call, same folder), disabled again in cleanup, with the catalog and the enabled map both polled first, plus the dropdown's own Refresh List as a bounded repair.num_models > 0and then re-read, catching the catalog mid-registration:124instead of248twice, and once an empty llm list, while the same endpoint seconds later reads{llm: 124, embeddings: 124}. Now one comparison over{llmIds, num_models, is_configured}, so a partial read is "not settled yet" rather than a wrong catalog.POST /api/v1/variables/writes answer 201/201 3/3 on1.12.0.dev19, and the test passes 3/3 through the Settings UI with its assertions untouched.test.fixmeremoved,@stableadded. Its rejection bodies are now captured, because an unreachable endpoint leaves the identical end state as the race:Invalid OpenAI-compatible base URLfails (the defect), a transport rejection skips with the body.400 … timed out; that shape is retried once, then skips with the reason. A drained account is read off the run's own message (no credits remaining/insufficient_quota) — invisible to the probe, sinceGET /v1/modelsanswers200for a key with no credit, and left unattributed it surfaces 90 s later as "AI reply … not persisted yet". Deliberately narrow:rate_limit_exceededstill fails.@stableis restored on test 5 (auto-removed bycb3082d).Validation
Langflow
1.12.0.dev19(=langflowai/langflow-nightly:latest,sha256:60cf62a2…),--workers=1 --retries=0.expected=6, unexpected=0, skipped=0(116 s)typecheck/lint/validate:specs/ both QA-CHECKLIST guardsOPENAI_COMPATIBLE_*variables, provider'senabled_modelsemptyForce-fail — all 6 tests verified red, then reverted:
No
// FF-MUTATIONmarker remains in the diff.Two findings NOT fixed here — separate issues to follow
tests/tests-automations/regression/mcp/client/mcp-client-agent.spec.ts:86carries a second inline copy of the same dead premise, spelledapi_key?.valuerather thancredential:— which is why the issue's grep missed it — inside a local 20 stoPass[Daily #744] agent/flow execution does not complete — div-chat-message / 'built successfully' never render (3 specs) #751 guard. It is@mcp @agents @regressionwith no@stable, so it never runs in the daily: latently broken, not currently red.gpt-4o-mini/OpenAI Compatibleat send time, and the run still failed with404 … This is not a chat model, which only the provider's first default-enabled id (babbage-002) produces. So it is not a persistence reversion and re-selecting cannot fix it —POST /api/v2/workflowsdid not build what the database holds. This PR attributes that state in one line rather than diagnosing it.Reviewer note
The validation machine's network stalls ~20 s on 2 of 12 (host) and 3 of 12 (container) calls to
api.openai.com, measured directly; it is not DNS or IPv6 (an/etc/hostspin and forced-4change nothing, and each IP answers in ~0.6 s when pinned). That is what the transport-skip paths above exist to attribute, and it is why some local runs show an explicit skip rather than a pass.🤖 Generated with Claude Code