Skip to content

feat: flag deprecated models and redesign admin models table#315

Open
MaxEriksson2000 wants to merge 17 commits intodevelopfrom
feature/flag-deprecated-models
Open

feat: flag deprecated models and redesign admin models table#315
MaxEriksson2000 wants to merge 17 commits intodevelopfrom
feature/flag-deprecated-models

Conversation

@MaxEriksson2000
Copy link
Copy Markdown
Collaborator

@MaxEriksson2000 MaxEriksson2000 commented Mar 31, 2026

Summary

This PR combines three related improvements to completion model management:

  1. Deprecated models are now clearly flagged in the admin UI.
  2. The admin models experience has been redesigned to make lifecycle state and actions easier to understand.
  3. The model migration lifecycle has been hardened so historical questions keep their original model attribution, while migrated or deleted models can still be cleaned up safely over time.

Why

We had two related problems in practice:

  • Deprecated models were hard to spot and still looked too similar to normal models in admin views.
  • Completion model migration treated historical question records like active configuration, which caused incorrect model attribution after migration and risked breaking usage analytics and audit trails.

This PR separates those concerns properly:

  • active configuration can be migrated,
  • historical question records stay attached to the original model,
  • old models can be retired and eventually cleaned up once no references remain.

What Changed

1. Deprecated model status in admin

  • Added explicit deprecated status indicators in the models table.
  • Added stronger visual treatment for deprecated rows and cells.
  • Added status icons/labels so deprecated and migrated models are easier to distinguish.
  • Added warning and migration guidance in the model detail dialog.

2. Admin model management UX redesign

  • Reworked the admin models table layout and interaction model.
  • Added a richer model detail dialog with lifecycle-aware actions.
  • Improved status visibility for enabled, deprecated, and migrated models.
  • Updated model labels and toggles so migrated models are visibly non-active.

3. Safer model migration lifecycle

Questions are historical records and are never migrated. Active configuration is migrated to the target model, while questions keep their original completion_model_id.

graph LR
    subgraph migration["Migration: A → B"]
        A["Model A<br/>(source)"] -->|"active config migrated"| B["Model B<br/>(target)"]
        A -.->|"migrated_to_model_id"| B
    end

    subgraph preserved["Preserved references"]
        Q1["Question 1"] -.->|"completion_model_id = A"| A
        Q2["Question 2"] -.->|"completion_model_id = A"| A
        Q3["Question 3"] -.->|"completion_model_id = A"| A
    end
Loading
  • Questions are no longer treated as migratable entities.
  • Historical questions keep their original completion_model_id.
  • After a successful migration, the source model is marked with migrated_to_model_id instead of being deleted during the migration flow.
  • Migrated models are excluded from availability checks so new assistants/apps/services cannot be created against them.
  • Soft-delete is used for completion model deletion, with lifecycle cleanup handling eventual hard-delete when references are gone.

Safety layers:

Layer Mechanism
Application can_access blocks migrated + deleted models in all 3 paths
Service is_completion_model_available() enforced in assistant/app/service creation
Space payload Filters out migrated + deleted models
Frontend Excludes migrated from target lists, space settings, locks enable switch
Database RESTRICT FK on questions.completion_model_id and migrated_to_model_id

4. Lifecycle cleanup for retired models

Added a weekly cleanup worker (ARQ cron, Sunday 4 AM) that hard-deletes models once all references are gone.

graph LR
    Start(( )) --> Active
    Active -->|LiteLLM flags| Deprecated
    Active -->|admin deletes| SoftDeleted

    Deprecated -->|admin migrates| Migrated
    Migrated -->|"cleanup job"| CleanedUp

    SoftDeleted -->|"cleanup job"| CleanedUp

    Migrated -. "questions still exist" .-> Migrated
    SoftDeleted -. "questions still exist" .-> SoftDeleted

    CleanedUp["Cleaned Up<br/>(hard-deleted, history preserved)"]
Loading

Cleanup eligibility — ALL must be true:

  1. Marked by lifecycle state (deleted_at IS NOT NULL or migrated_to_model_id IS NOT NULL)
  2. Zero questions referencing it
  3. Zero active entity references (assistants, apps, services, spaces, non-deleted templates)
  4. No other model has migrated_to_model_id pointing to it

Soft-deleted templates are reconciled (FK cleared) before hard-delete. IntegrityError from RESTRICT FK is classified as db_restrict (skip, not error).

5. Migration history and auditability

History records survive model cleanup via snapshot fields, not dead UUIDs.

graph TD
    subgraph before["Before cleanup"]
        H["Migration History<br/>from_model_id = A (FK)<br/>to_model_id = B (FK)<br/>from_model_name = gpt-3.5-turbo<br/>from_provider_type = openai"]
        MA["Model A"]
        MB["Model B"]
        H -->|FK| MA
        H -->|FK| MB
    end

    subgraph after["After cleanup removes Model A"]
        H2["Migration History<br/>from_model_id = NULL (SET NULL)<br/>to_model_id = B (FK)<br/>from_model_name = gpt-3.5-turbo ✓<br/>from_provider_type = openai ✓"]
        MB2["Model B"]
        H2 -->|FK| MB2
    end
Loading
  • from_model_name / to_model_name — snapshot of CompletionModels.name at migration time
  • from_provider_type / to_provider_type — snapshot of provider for audit readability
  • from_model_id / to_model_id — live FK, becomes NULL after cleanup (by design)
  • Per-model history lookup works for live models; use tenant history or migration_id after cleanup

Semantics to Note

  • Questions are historical events and are intentionally never migrated.
  • from_model_id / to_model_id in migration history only represent live model rows.
  • After hard-delete, model-specific history by old model id is no longer expected to work; history is instead preserved via the migration record and snapshot fields.
  • This is intentional and should be considered part of the lifecycle model, not a regression.

Testing

Added and updated focused unit and integration coverage for:

  • deprecated model enrichment and visibility
  • migration validation
  • question attribution invariants
  • migrated/deleted model access control
  • cleanup lifecycle behavior
  • migration history semantics
  • sysadmin completion model delete behavior

All 151 completion model + AI model tests pass.

Closes

Closes #316
Closes #317
Closes #320
Closes #323

Enrich API responses with deprecation_date from litellm.model_cost at
serialization time. Models past their deprecation date are automatically
flagged as deprecated. The frontend shows red/yellow labels in the
Details column and exposes a Migrate action directly in the dropdown
menu for deprecated completion models.

Also adds SDK functions for migration history endpoints (already existed
in backend but were never wired to frontend) and a Migration History
tab in the admin models page.

Backend:
- New deprecation_lookup utility with get_litellm_deprecation_date()
- deprecation_date field on CompletionModelPublic, EmbeddingModelPublic,
  TranscriptionModelPublic (computed at serialization, no DB migration)
- 21 new unit tests covering lookup and enrichment logic

Frontend:
- Red "Deprecated" / yellow "Retiring YYYY-MM-DD" labels in ModelLabels
- Migrate button in ModelActions dropdown for deprecated models
- getMigrationHistory/getAllMigrationHistory SDK functions
- MigrationHistoryPanel component + tab in admin/models page
- Translation keys for EN and SV
Replace the dense tag-based table with a cleaner design:
- Remove Details column tags, replace with compact status icons
  (ModelStatusIcons: deprecation, reasoning, vision, tools)
- Remove Security column (moved to ModelDetailDialog)
- Add status indicator dot on each model name (green/red/yellow/gray)
- New ModelDetailDialog: click model name to see full details
  (capabilities, hosting, security, metadata, deprecation alert)
- Deprecation warning banner above table with model count
- Row tinting for deprecated/retiring models (CSS :has selector)
- Reorder dropdown actions: neutral first, destructive last
- Migrate action available for all completion models, not just deprecated
- Enable Vite polling for HMR in Docker dev environment
…istory

Migration validation:
- New GET /completion-models/{id}/migration-validate endpoint for preflight
- Server-side compatibility check on target selection (no client-side approx)
- Security classification blocker: prevents migration to lower-classified model
  when spaces require higher classification (cannot be overridden)
- Race condition fix: stale validation responses discarded
- Dual warning format: human-readable warnings + machine-readable warning_codes

Migration execution:
- Fix kwargs reset being a blocker (now informational only)
- Remove auto-delete that ran even on failure; delete only after success
- Spaces included in default migration via MIGRATABLE_ENTITY_TYPES
- Audit logging with COMPLETION_MODEL_MIGRATED action type

Migration history:
- Alembic migration: FK ondelete CASCADE → SET NULL, add model name columns
- Store from_model_name/to_model_name at creation (survives model deletion)
- ModelMigrationHistory: model IDs now Optional[UUID]
- Expandable detail rows: breakdown per entity type, warnings, errors, duration
- Auto-refresh via migrationHistoryRefreshVersion store

Migration dialog UX:
- Impact preview: shows affected resources grouped by type with details
- Collapsible sections per entity type (assistants, apps, questions)
- Spaces info: "Target model will be enabled on N spaces"
- Preflight warnings shown on target selection with translated messages
- Security blocker shown as red panel, disables migrate button
- SDK: validateMigration, getUsageStats, getUsageDetails functions
48 new unit tests across 3 files:
- test_migration_validation.py (22): compatibility checks, security blockers,
  warning codes, kwargs reset, multiple warnings combined
- test_migration_history.py (15): stored model names, SET NULL handling,
  Pydantic nullable IDs, fallback logic, serialization roundtrip
- test_migration_endpoint.py (11): audit action type, category mapping,
  metadata, ValidationResult/MigrationRequest schema tests

Updated existing tests for warning_codes field and audit action registration.
@MaxEriksson2000 MaxEriksson2000 marked this pull request as ready for review April 2, 2026 09:29
- Fix security classification blocker check: was searching for codes in
  warnings (human-readable text) instead of warning_codes. With
  confirm_migration=true, a security-blocked migration could pass through.
- Fix impact preview total: usage/details endpoint now counts real total
  across all entity types via separate COUNT queries, instead of returning
  len(items) which was just the current page size.
Move scattered model settings into a unified flow:
- ModelDetailDialog is now read-only info view
- EditModelDialog handles all editing: parameters, security
  classification, default model status
- ModelActions dropdown simplified to Edit, Migrate, Delete
- Table status dots: red=disabled, icons for deprecated/retiring
- SelectSecurityClassification trigger now full-width
Security classification can now be set when adding a model, so admins
don't need to create and then immediately edit to set classification.
Uses a separate API call after create since the backend create endpoint
doesn't accept security_classification directly.
Table name column now shows only the display name — model identifier
and context window moved to the detail dialog. Detail dialog always
shows both display name and model identifier with consistent styling.
Questions are historical records that must not be migrated — doing so
falsely attributes answers to the target model and corrupts token usage
analytics.

Changes:
- Exclude questions from MIGRATABLE_ENTITY_TYPES
- Add migrated_to_model_id (FK RESTRICT) and deleted_at to completion_models
- Change questions FK from SET NULL to RESTRICT as safety net
- Mark source model with migrated_to_model_id after successful migration
- Block re-migration of already-migrated models (both preflight and execute)
- Soft-delete replaces hard-delete in both tenant and sysadmin endpoints
- Update can_access in all three paths (domain, AIModelsService, pydantic)
- Filter migrated/deleted models from space assembler, model selection UIs
- Add MODEL_IN_USE check before soft-delete via shared has_active_references
- Frontend: remove auto-delete after migration, add Migrated label, lock
  enable switch, exclude migrated from target lists and space settings
Automatically hard-deletes completion models once all references are gone
(questions gallrad, active entities migrated, no migration pointers).

- Weekly ARQ cron job (Sunday 4 AM) targeting soft-deleted and migrated models
- Reuses shared has_active_references from CompletionModelsRepository
- Reconciles soft-deleted template FK references before hard-delete
- IntegrityError classified as db_restrict skip, not unexpected error
- Preserves migration history via immutable from/to_model_original_id columns
- History service falls back to original_id when FK is SET NULL after cleanup
- Indexes on deleted_at, migrated_to_model_id, and original_id columns
… migration history

Simplify the migration history model: remove from/to_model_original_id
(dead UUIDs pointing to deleted rows) and replace with provider_type
snapshots that give readable audit context.

- Remove from_model_original_id and to_model_original_id columns
- Add from_provider_type and to_provider_type as audit snapshots
- Backfill provider_type from model_providers for existing records
- Simplify repo queries to only match live FK IDs (no OR fallback)
- History service returns None for model IDs after cleanup (not stale UUIDs)
- Per-model history works for live models; use tenant/migration_id after cleanup
Fixes svelte-package failing to generate type declarations due to
inferred types referencing internal @melt-ui/svelte modules.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants