Skip to content

Commit 42f2714

Browse files
authored
Merge branch 'release-1.11.0' into fix/LE-1738-remaining-rbac-gaps
2 parents cb74f16 + 09d77c4 commit 42f2714

8 files changed

Lines changed: 99 additions & 30 deletions

File tree

src/backend/base/langflow/api/v1/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,10 +833,32 @@ async def update_enabled_models(
833833
disabled_models = normalize_model_status_entries(disabled_models, providers_by_name)
834834
explicitly_enabled_models = normalize_model_status_entries(explicitly_enabled_models, providers_by_name)
835835

836+
unavailable_models: dict[tuple[str, str], str] = {}
837+
for provider_dict in all_models_by_provider:
838+
provider = provider_dict.get("provider")
839+
if not isinstance(provider, str):
840+
continue
841+
for model in provider_dict.get("models", []):
842+
model_name = model.get("model_name")
843+
if not isinstance(model_name, str):
844+
continue
845+
metadata = model.get("metadata", {})
846+
if metadata.get("deprecated", False):
847+
unavailable_models[(provider, model_name)] = "deprecated"
848+
elif metadata.get("not_supported", False):
849+
unavailable_models[(provider, model_name)] = "not supported"
850+
836851
# Update model sets based on user requests
837852
# For any model being enabled, validate the provider credentials
838853
for update in updates:
839854
if update.enabled:
855+
unavailable_reason = unavailable_models.get((update.provider, update.model_id))
856+
if unavailable_reason:
857+
raise HTTPException(
858+
status_code=400,
859+
detail=f"Cannot enable {unavailable_reason} model: {update.model_id}",
860+
)
861+
840862
from lfx.base.models.unified_models import get_all_variables_for_provider, validate_model_provider_key
841863

842864
# Get variables from DB or environment

src/backend/tests/unit/api/v1/test_models_enabled_providers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,19 @@ async def test_enabled_providers_reflects_models_endpoint(client: AsyncClient, o
365365
assert enabled_result["provider_status"]["OpenAI"] is True
366366

367367

368+
@pytest.mark.usefixtures("active_user")
369+
async def test_cannot_enable_deprecated_model(client: AsyncClient, logged_in_headers):
370+
"""Deprecated models cannot be persisted as explicitly enabled."""
371+
response = await client.post(
372+
"api/v1/models/enabled_models",
373+
json=[{"provider": "OpenAI", "model_id": "gpt-3.5-turbo", "enabled": True}],
374+
headers=logged_in_headers,
375+
)
376+
377+
assert response.status_code == status.HTTP_400_BAD_REQUEST
378+
assert response.json()["detail"] == "Cannot enable deprecated model: gpt-3.5-turbo"
379+
380+
368381
@pytest.mark.usefixtures("active_user")
369382
async def test_security_credential_value_never_exposed_in_variables_endpoint(
370383
client: AsyncClient, openai_credential, logged_in_headers

src/backend/tests/unit/api/v1/test_variable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ async def test_delete_provider_credential_cleans_up_enabled_models(client: Async
599599
enable_response = await client.post(
600600
"api/v1/models/enabled_models",
601601
json=[
602-
{"provider": "OpenAI", "model_id": "gpt-4-turbo-preview", "enabled": True},
602+
{"provider": "OpenAI", "model_id": "gpt-4.1-mini", "enabled": True},
603603
],
604604
headers=logged_in_headers,
605605
)
@@ -620,7 +620,7 @@ async def test_delete_provider_credential_cleans_up_enabled_models(client: Async
620620
import json
621621

622622
enabled_models = json.loads(enabled_models_var["value"])
623-
assert "gpt-4-turbo-preview" not in enabled_models
623+
assert "gpt-4.1-mini" not in enabled_models
624624

625625

626626
@pytest.mark.usefixtures("active_user")

src/frontend/src/modals/modelProviderModal/__tests__/ModelSelection.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,26 @@ describe("ModelSelection", () => {
532532
);
533533
});
534534

535+
it("should not allow deprecated models to be enabled", async () => {
536+
const user = userEvent.setup();
537+
const onModelToggle = jest.fn();
538+
render(
539+
<ModelSelection
540+
{...defaultProps}
541+
availableModels={withDeprecated}
542+
modelType="llm"
543+
onModelToggle={onModelToggle}
544+
/>,
545+
);
546+
547+
await user.click(screen.getByTestId("llm-deprecated-summary"));
548+
const deprecatedToggle = screen.getByTestId("llm-toggle-gpt-old");
549+
expect(deprecatedToggle).toBeDisabled();
550+
551+
await user.click(deprecatedToggle);
552+
expect(onModelToggle).not.toHaveBeenCalled();
553+
});
554+
535555
it("should pluralize 'model' correctly for a single deprecated row", () => {
536556
const single: Model[] = [
537557
{ model_name: "gpt-4o", metadata: { model_type: "llm", icon: "Bot" } },

src/frontend/src/modals/modelProviderModal/components/ModelSelection.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ const ModelRow = ({
157157
{isEnabledModel && (
158158
<Switch
159159
checked={enabled}
160+
disabled={
161+
model.metadata?.deprecated === true ||
162+
model.metadata?.not_supported === true
163+
}
160164
onCheckedChange={(checked) =>
161165
onToggle(model.model_name, checked, modelType)
162166
}
@@ -308,9 +312,7 @@ const ModelSelection = ({
308312
);
309313

310314
const addableDeploymentTypes =
311-
supportsCustomDeployments &&
312-
!!isEnabledModel &&
313-
trimmedModelQuery.length > 0
315+
supportsCustomDeployments && isEnabledModel && trimmedModelQuery.length > 0
314316
? visibleDeploymentTypes.filter(
315317
(deploymentType) =>
316318
!modelsWithCustomDeployments.some(

src/lfx/src/lfx/base/models/models_dev_catalog.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,9 @@
5757

5858
# Threshold for auto-deprecating models that haven't shipped a new version in
5959
# a long time. models.dev has no deprecation field, and providers rarely
60-
# formally deprecate models even after they ship a successor — but a model
61-
# that hasn't been touched in 30 months is overwhelmingly legacy in practice
62-
# (catches gpt-4 / gpt-4-turbo / text-embedding-ada-002 today, leaves
63-
# gpt-4o / text-embedding-3-small/large active). Tuned conservatively so
64-
# current embeddings (~28 months old) survive; nudge down only if successor
65-
# adoption is verified.
60+
# formally deprecate models even after they ship a successor. Apply this only
61+
# to language models: embedding release cadences are slower, and age alone
62+
# does not mean that a provider has deprecated an embedding model.
6663
_AGE_DEPRECATION_DAYS = 900
6764

6865
MODELS_DEV_URL = "https://models.dev/api.json"
@@ -229,15 +226,16 @@ def _is_embedding_family(model_dict: dict[str, Any]) -> bool:
229226

230227

231228
def _is_aged_out(model_dict: dict[str, Any], now: datetime) -> bool:
232-
"""Return True if the model is older than ``_AGE_DEPRECATION_DAYS`` days.
229+
"""Return True if a language model is older than ``_AGE_DEPRECATION_DAYS`` days.
233230
234231
Prefers ``release_date`` (when the model functionally shipped) over
235232
``last_updated`` (which tracks catalog-curator edits like typo fixes, not
236-
new model versions). The combination of preferring release_date and the
237-
900-day threshold correctly catches ``gpt-4`` / ``gpt-4-turbo`` (924d from
238-
their 2023-11 release) while keeping ``text-embedding-3-large`` (844d
239-
from its 2024-01 release) active.
233+
new model versions). Embedding models are excluded because their age does
234+
not indicate provider deprecation.
240235
"""
236+
if _is_embedding_family(model_dict):
237+
return False
238+
241239
raw = model_dict.get("release_date") or model_dict.get("last_updated")
242240
epoch = _release_date_to_epoch(raw)
243241
if epoch == 0:
@@ -285,7 +283,7 @@ def _translate_model_entry(
285283
``deprecated`` kwarg (which lets callers forward the static-list
286284
curation that models.dev itself doesn't surface):
287285
* dated-snapshot id (:data:`_DATED_SNAPSHOT_RE`);
288-
* stale ``last_updated`` / ``release_date`` per :data:`_AGE_DEPRECATION_DAYS`.
286+
* stale LLM ``last_updated`` / ``release_date`` per :data:`_AGE_DEPRECATION_DAYS`.
289287
``now`` is injected for testability — defaults to the current UTC time.
290288
"""
291289
now = now or datetime.now(tz=timezone.utc)
@@ -357,8 +355,8 @@ def apply_models_dev_overrides(
357355
preserves the static-list curation by name: any model that was already
358356
flagged deprecated in the bundled ``*_constants.py`` lists keeps that flag
359357
after the override. Dated-snapshot ids
360-
(e.g. ``claude-opus-4-5-20251101``, ``gpt-4o-2024-05-13``) and rows whose
361-
most recent date is older than :data:`_AGE_DEPRECATION_DAYS` are also
358+
(e.g. ``claude-opus-4-5-20251101``, ``gpt-4o-2024-05-13``) and language
359+
models whose most recent date is older than :data:`_AGE_DEPRECATION_DAYS` are also
362360
auto-flagged in :func:`_translate_model_entry`. ``now`` is forwarded for
363361
testability.
364362
"""

src/lfx/src/lfx/graph/flow_builder/layout.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
from lfx.graph.flow_builder._utils import node_id as _node_id
1212

13-
LAYER_SPACING_X = 600
13+
# Canvas nodes render 384px wide (frontend NODE_WIDTH), so column spacing
14+
# of 480 leaves a ~96px visual gap between layers.
15+
LAYER_SPACING_X = 480
1416
NODE_SPACING_Y = 350
1517

1618

src/lfx/tests/unit/base/models/test_models_dev_catalog.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,12 @@ def test_apply_overrides_preserves_static_deprecated_flag():
454454
{"provider": "OpenAI", "name": "gpt-4o", "tool_calling": True, "deprecated": False},
455455
{"provider": "OpenAI", "name": "gpt-3.5-turbo", "tool_calling": True, "deprecated": True},
456456
{"provider": "OpenAI", "name": "gpt-4.5-preview", "tool_calling": True, "deprecated": True},
457+
{
458+
"provider": "OpenAI",
459+
"name": "text-embedding-legacy",
460+
"model_type": "embeddings",
461+
"deprecated": True,
462+
},
457463
]
458464
snapshot = {
459465
"openai": {
@@ -465,6 +471,11 @@ def test_apply_overrides_preserves_static_deprecated_flag():
465471
# New model not in our static list — should default to
466472
# non-deprecated unless dated-snapshot.
467473
"gpt-6": {"id": "gpt-6", "tool_call": True},
474+
"text-embedding-legacy": {
475+
"id": "text-embedding-legacy",
476+
"family": "text-embedding",
477+
"release_date": "2020-01-01",
478+
},
468479
},
469480
}
470481
}
@@ -475,6 +486,7 @@ def test_apply_overrides_preserves_static_deprecated_flag():
475486
assert by_name["gpt-3.5-turbo"]["deprecated"] is True
476487
assert by_name["gpt-4.5-preview"]["deprecated"] is True
477488
assert by_name["gpt-6"]["deprecated"] is False
489+
assert by_name["text-embedding-legacy"]["deprecated"] is True
478490

479491

480492
def test_apply_overrides_marks_embedding_family_as_embeddings_model_type():
@@ -517,37 +529,37 @@ def test_apply_overrides_marks_embedding_family_as_embeddings_model_type():
517529
assert by_name["voyage-3-embedding"]["model_type"] == "embeddings"
518530

519531

520-
def test_apply_overrides_auto_deprecates_stale_models():
521-
"""Models with last_updated older than ~30 months are auto-deprecated.
532+
def test_apply_overrides_auto_deprecates_stale_language_models_only():
533+
"""Language models older than ~30 months are auto-deprecated.
522534
523-
Catches gpt-4 / gpt-4-turbo / text-embedding-ada-002 today; leaves
524-
gpt-4o / text-embedding-3-* / current Claudes active.
535+
Embedding models remain active regardless of age unless the provider's
536+
static catalog explicitly marks them deprecated.
525537
"""
526538
from lfx.base.models.models_dev_catalog import apply_models_dev_overrides
527539

528-
fixed_now = datetime(2026, 5, 18, tzinfo=timezone.utc)
540+
fixed_now = datetime(2026, 7, 14, tzinfo=timezone.utc)
529541
snapshot = {
530542
"openai": {
531543
"id": "openai",
532544
"models": {
533-
# 2023-11 → 924 days as of 2026-05-18 → deprecated
545+
# 2023-11 → older than 900 days → deprecated
534546
"gpt-4": {"id": "gpt-4", "release_date": "2023-11-06", "last_updated": "2024-04-09"},
535547
"gpt-4-turbo": {"id": "gpt-4-turbo", "release_date": "2023-11-06", "last_updated": "2024-04-09"},
536-
# 2022-12 → very old → deprecated
548+
# 2022-12 → very old, but embeddings are exempt from the age heuristic
537549
"text-embedding-ada-002": {
538550
"id": "text-embedding-ada-002",
539551
"family": "text-embedding",
540552
"release_date": "2022-12-15",
541553
"last_updated": "2022-12-15",
542554
},
543-
# 2024-01-25 → 844 days still active under the 900d threshold
555+
# 2024-01-25 → over 900 days, but still active because it is an embedding
544556
"text-embedding-3-large": {
545557
"id": "text-embedding-3-large",
546558
"family": "text-embedding",
547559
"release_date": "2024-01-25",
548560
"last_updated": "2024-01-25",
549561
},
550-
# 2024-08650 days → active
562+
# 2024-05newer than 900 days → active
551563
"gpt-4o": {"id": "gpt-4o", "release_date": "2024-05-13", "last_updated": "2024-08-06"},
552564
# No date at all → not auto-deprecated (insufficient signal)
553565
"gpt-future": {"id": "gpt-future"},
@@ -560,7 +572,7 @@ def test_apply_overrides_auto_deprecates_stale_models():
560572

561573
assert by_name["gpt-4"]["deprecated"] is True
562574
assert by_name["gpt-4-turbo"]["deprecated"] is True
563-
assert by_name["text-embedding-ada-002"]["deprecated"] is True
575+
assert by_name["text-embedding-ada-002"]["deprecated"] is False
564576
assert by_name["text-embedding-3-large"]["deprecated"] is False
565577
assert by_name["gpt-4o"]["deprecated"] is False
566578
assert by_name["gpt-future"]["deprecated"] is False

0 commit comments

Comments
 (0)