diff --git a/core/application/config_file_watcher.go b/core/application/config_file_watcher.go index 7d174bede..e99de63e6 100644 --- a/core/application/config_file_watcher.go +++ b/core/application/config_file_watcher.go @@ -338,6 +338,9 @@ func readRuntimeSettingsJson(startupAppConfig config.ApplicationConfig) fileHand if settings.AutoUpgradeBackends != nil { appConfig.AutoUpgradeBackends = *settings.AutoUpgradeBackends } + if settings.PreferDevelopmentBackends != nil { + appConfig.PreferDevelopmentBackends = *settings.PreferDevelopmentBackends + } if settings.ApiKeys != nil { // API keys from env vars (startup) should be kept, runtime settings keys replace all runtime keys // If runtime_settings.json specifies ApiKeys (even if empty), it replaces all runtime keys diff --git a/core/cli/run.go b/core/cli/run.go index fe3f7c212..1c27844c5 100644 --- a/core/cli/run.go +++ b/core/cli/run.go @@ -47,8 +47,9 @@ type RunCMD struct { BackendImagesReleaseTag string `env:"LOCALAI_BACKEND_IMAGES_RELEASE_TAG,BACKEND_IMAGES_RELEASE_TAG" help:"Fallback release tag for backend images" group:"backends" default:"latest"` BackendImagesBranchTag string `env:"LOCALAI_BACKEND_IMAGES_BRANCH_TAG,BACKEND_IMAGES_BRANCH_TAG" help:"Fallback branch tag for backend images" group:"backends" default:"master"` BackendDevSuffix string `env:"LOCALAI_BACKEND_DEV_SUFFIX,BACKEND_DEV_SUFFIX" help:"Development suffix for backend images" group:"backends" default:"development"` - AutoUpgradeBackends bool `env:"LOCALAI_AUTO_UPGRADE_BACKENDS,AUTO_UPGRADE_BACKENDS" help:"Automatically upgrade backends when new versions are detected" group:"backends" default:"false"` - PreloadModels string `env:"LOCALAI_PRELOAD_MODELS,PRELOAD_MODELS" help:"A List of models to apply in JSON at start" group:"models"` + AutoUpgradeBackends bool `env:"LOCALAI_AUTO_UPGRADE_BACKENDS,AUTO_UPGRADE_BACKENDS" help:"Automatically upgrade backends when new versions are detected" group:"backends" default:"false"` + PreferDevelopmentBackends bool `env:"LOCALAI_PREFER_DEV_BACKENDS,PREFER_DEV_BACKENDS" help:"Prefer development backend versions (shows development backends by default in UI)" group:"backends" default:"false"` + PreloadModels string `env:"LOCALAI_PRELOAD_MODELS,PRELOAD_MODELS" help:"A List of models to apply in JSON at start" group:"models"` Models []string `env:"LOCALAI_MODELS,MODELS" help:"A List of model configuration URLs to load" group:"models"` PreloadModelsConfig string `env:"LOCALAI_PRELOAD_MODELS_CONFIG,PRELOAD_MODELS_CONFIG" help:"A List of models to apply at startup. Path to a YAML config file" group:"models"` @@ -495,6 +496,10 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error { opts = append(opts, config.WithAutoUpgradeBackends(r.AutoUpgradeBackends)) } + if r.PreferDevelopmentBackends { + opts = append(opts, config.WithPreferDevelopmentBackends(r.PreferDevelopmentBackends)) + } + if r.PreloadBackendOnly { _, err := application.New(opts...) return err diff --git a/core/config/application_config.go b/core/config/application_config.go index e22ad4d0c..84f6abcbe 100644 --- a/core/config/application_config.go +++ b/core/config/application_config.go @@ -58,6 +58,7 @@ type ApplicationConfig struct { AutoloadGalleries, AutoloadBackendGalleries bool AutoUpgradeBackends bool + PreferDevelopmentBackends bool SingleBackend bool // Deprecated: use MaxActiveBackends = 1 instead MaxActiveBackends int // Maximum number of active backends (0 = unlimited, 1 = single backend mode) @@ -395,6 +396,10 @@ func WithAutoUpgradeBackends(v bool) AppOption { return func(o *ApplicationConfig) { o.AutoUpgradeBackends = v } } +func WithPreferDevelopmentBackends(v bool) AppOption { + return func(o *ApplicationConfig) { o.PreferDevelopmentBackends = v } +} + var EnableFederated = func(o *ApplicationConfig) { o.Federated = true } @@ -868,6 +873,7 @@ func (o *ApplicationConfig) ToRuntimeSettings() RuntimeSettings { autoloadGalleries := o.AutoloadGalleries autoloadBackendGalleries := o.AutoloadBackendGalleries autoUpgradeBackends := o.AutoUpgradeBackends + preferDevelopmentBackends := o.PreferDevelopmentBackends apiKeys := o.ApiKeys agentJobRetentionDays := o.AgentJobRetentionDays @@ -942,6 +948,7 @@ func (o *ApplicationConfig) ToRuntimeSettings() RuntimeSettings { AutoloadGalleries: &autoloadGalleries, AutoloadBackendGalleries: &autoloadBackendGalleries, AutoUpgradeBackends: &autoUpgradeBackends, + PreferDevelopmentBackends: &preferDevelopmentBackends, ApiKeys: &apiKeys, AgentJobRetentionDays: &agentJobRetentionDays, OpenResponsesStoreTTL: &openResponsesStoreTTL, @@ -1093,6 +1100,9 @@ func (o *ApplicationConfig) ApplyRuntimeSettings(settings *RuntimeSettings) (req if settings.AutoUpgradeBackends != nil { o.AutoUpgradeBackends = *settings.AutoUpgradeBackends } + if settings.PreferDevelopmentBackends != nil { + o.PreferDevelopmentBackends = *settings.PreferDevelopmentBackends + } if settings.AgentJobRetentionDays != nil { o.AgentJobRetentionDays = *settings.AgentJobRetentionDays } diff --git a/core/config/runtime_settings.go b/core/config/runtime_settings.go index 21a112b4d..4c2555a81 100644 --- a/core/config/runtime_settings.go +++ b/core/config/runtime_settings.go @@ -20,7 +20,8 @@ type RuntimeSettings struct { // Backend management SingleBackend *bool `json:"single_backend,omitempty"` // Deprecated: use MaxActiveBackends = 1 instead MaxActiveBackends *int `json:"max_active_backends,omitempty"` // Maximum number of active backends (0 = unlimited, 1 = single backend mode) - AutoUpgradeBackends *bool `json:"auto_upgrade_backends,omitempty"` // Automatically upgrade backends when new versions are detected + AutoUpgradeBackends *bool `json:"auto_upgrade_backends,omitempty"` // Automatically upgrade backends when new versions are detected + PreferDevelopmentBackends *bool `json:"prefer_development_backends,omitempty"` // Prefer development backend versions by default in UI // Memory Reclaimer settings (works with GPU if available, otherwise RAM) MemoryReclaimerEnabled *bool `json:"memory_reclaimer_enabled,omitempty"` // Enable memory threshold monitoring MemoryReclaimerThreshold *float64 `json:"memory_reclaimer_threshold,omitempty"` // Threshold 0.0-1.0 (e.g., 0.95 = 95%) diff --git a/core/gallery/backend_types.go b/core/gallery/backend_types.go index ef5904f99..3aa6898d6 100644 --- a/core/gallery/backend_types.go +++ b/core/gallery/backend_types.go @@ -2,6 +2,7 @@ package gallery import ( "fmt" + "strings" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/pkg/system" @@ -70,6 +71,13 @@ func (m *GalleryBackend) IsMeta() bool { return len(m.CapabilitiesMap) > 0 && m.URI == "" } +func (m *GalleryBackend) IsDevelopment(devSuffix string) bool { + if devSuffix == "" { + devSuffix = defaultDevSuffix + } + return strings.HasSuffix(m.Name, "-"+devSuffix) +} + // IsCompatibleWith checks if the backend is compatible with the current system capability. // For meta backends, it checks if any of the capabilities in the map match the system capability. // For concrete backends, it delegates to SystemState.IsBackendCompatible. diff --git a/core/gallery/upgrade.go b/core/gallery/upgrade.go index aec75852c..dde33300f 100644 --- a/core/gallery/upgrade.go +++ b/core/gallery/upgrade.go @@ -72,14 +72,28 @@ func CheckBackendUpgrades(ctx context.Context, galleries []config.Gallery, syste continue } - // If either version is empty, fall back to OCI digest comparison - if installed.Metadata.Digest != "" && downloader.URI(galleryEntry.URI).LooksLikeOCI() { + // Gallery has a version but installed doesn't — this happens for backends + // installed before version tracking was added. Flag as upgradeable so + // users can re-install to pick up version metadata. + if galleryVersion != "" && installedVersion == "" { + result[installed.Metadata.Name] = UpgradeInfo{ + BackendName: installed.Metadata.Name, + InstalledVersion: "", + AvailableVersion: galleryVersion, + } + continue + } + + // Fall back to OCI digest comparison when versions are unavailable + if downloader.URI(galleryEntry.URI).LooksLikeOCI() { remoteDigest, err := oci.GetImageDigest(galleryEntry.URI, "", nil, nil) if err != nil { xlog.Warn("Failed to get remote OCI digest for upgrade check", "backend", installed.Metadata.Name, "error", err) continue } - if remoteDigest != installed.Metadata.Digest { + // If we have a stored digest, compare; otherwise any remote digest + // means we can't confirm we're up to date — flag as upgradeable + if installed.Metadata.Digest == "" || remoteDigest != installed.Metadata.Digest { result[installed.Metadata.Name] = UpgradeInfo{ BackendName: installed.Metadata.Name, InstalledDigest: installed.Metadata.Digest, @@ -87,7 +101,7 @@ func CheckBackendUpgrades(ctx context.Context, galleries []config.Gallery, syste } } } - // No version info and no digest to compare — skip + // No version info and non-OCI URI — cannot determine, skip } return result, nil diff --git a/core/http/react-ui/package-lock.json b/core/http/react-ui/package-lock.json index 356014dec..e1a449c1e 100644 --- a/core/http/react-ui/package-lock.json +++ b/core/http/react-ui/package-lock.json @@ -8,7 +8,16 @@ "name": "localai-react-ui", "version": "0.0.1", "dependencies": { + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.11.0", + "@codemirror/lint": "^6.8.5", + "@codemirror/search": "^6.5.10", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.36.8", "@fortawesome/fontawesome-free": "^6.7.2", + "@lezer/highlight": "^1.2.1", "@modelcontextprotocol/ext-apps": "^1.2.2", "@modelcontextprotocol/sdk": "^1.25.1", "dompurify": "^3.2.5", @@ -16,7 +25,8 @@ "marked": "^15.0.7", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.6.1" + "react-router-dom": "^7.6.1", + "yaml": "^2.8.3" }, "devDependencies": { "@eslint/js": "^9.27.0", @@ -292,6 +302,94 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz", + "integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -966,6 +1064,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, "node_modules/@modelcontextprotocol/ext-apps": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.2.2.tgz", @@ -1829,6 +1963,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3537,6 +3676,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3727,6 +3871,11 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3761,6 +3910,20 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/core/http/react-ui/src/pages/Backends.jsx b/core/http/react-ui/src/pages/Backends.jsx index 3cbb71872..61940628d 100644 --- a/core/http/react-ui/src/pages/Backends.jsx +++ b/core/http/react-ui/src/pages/Backends.jsx @@ -7,6 +7,7 @@ import { useOperations } from '../hooks/useOperations' import LoadingSpinner from '../components/LoadingSpinner' import { renderMarkdown } from '../utils/markdown' import ConfirmDialog from '../components/ConfirmDialog' +import Toggle from '../components/Toggle' export default function Backends() { const { addToast } = useOutletContext() @@ -27,6 +28,10 @@ export default function Backends() { const [confirmDialog, setConfirmDialog] = useState(null) const [allBackends, setAllBackends] = useState([]) const [upgrades, setUpgrades] = useState({}) + const [upgradingAll, setUpgradingAll] = useState(false) + const [showAllBackends, setShowAllBackends] = useState(false) + const [showDevelopment, setShowDevelopment] = useState(false) + const [preferDevLoaded, setPreferDevLoaded] = useState(false) const fetchBackends = useCallback(async () => { try { @@ -37,6 +42,11 @@ export default function Backends() { const list = Array.isArray(data?.backends) ? data.backends : Array.isArray(data) ? data : [] setAllBackends(list) setInstalledCount(list.filter(b => b.installed).length) + // On first load, use server preference for development toggle + if (!preferDevLoaded && data?.preferDevelopmentBackends) { + setShowDevelopment(true) + setPreferDevLoaded(true) + } } catch (err) { addToast(`Failed to load backends: ${err.message}`, 'error') } finally { @@ -60,17 +70,33 @@ export default function Backends() { .catch(() => {}) }, [operations.length]) - // Client-side filtering by tag - const filteredBackends = filter - ? allBackends.filter(b => { + // Client-side filtering by meta/development toggles and tag + const filteredBackends = (() => { + let result = allBackends + + // Show only meta backends unless "Show all" is toggled + if (!showAllBackends) { + result = result.filter(b => b.isMeta) + } + + // Hide development backends unless toggled on + if (!showDevelopment) { + result = result.filter(b => !b.isDevelopment) + } + + // Apply tag filter + if (filter) { + result = result.filter(b => { const tags = (b.tags || []).map(t => t.toLowerCase()) const name = (b.name || '').toLowerCase() const desc = (b.description || '').toLowerCase() const f = filter.toLowerCase() - // Match against tags, or name/description containing the filter keyword return tags.some(t => t.includes(f)) || name.includes(f) || desc.includes(f) }) - : allBackends + } + + return result + })() // Client-side pagination const ITEMS_PER_PAGE = 21 @@ -131,6 +157,22 @@ export default function Backends() { } } + const handleUpgradeAll = async () => { + const names = Object.keys(upgrades) + if (names.length === 0) return + setUpgradingAll(true) + try { + for (const name of names) { + await backendsApi.upgrade(name) + } + addToast(`Upgrading ${names.length} backend${names.length > 1 ? 's' : ''}...`, 'info') + } catch (err) { + addToast(`Upgrade failed: ${err.message}`, 'error') + } finally { + setUpgradingAll(false) + } + } + const handleManualInstall = async (e) => { e.preventDefault() if (!manualUri.trim()) { addToast('Please enter a URI', 'warning'); return } @@ -154,6 +196,9 @@ export default function Backends() { return operations.find(op => op.name === backend.name || op.name === backend.id) || null } + const handleToggleAllBackends = () => { setShowAllBackends(v => !v); setPage(1) } + const handleToggleDev = () => { setShowDevelopment(v => !v); setPage(1) } + const FILTERS = [ { key: '', label: 'All', icon: 'fa-layer-group' }, { key: 'llm', label: 'LLM', icon: 'fa-brain' }, @@ -211,6 +256,33 @@ export default function Backends() { + {/* Upgrade Banner */} + {Object.keys(upgrades).length > 0 && ( +
+
+ + + {Object.keys(upgrades).length} backend{Object.keys(upgrades).length > 1 ? 's have' : ' has'} updates available + +
+ +
+ )} + {/* Manual Install */}
-
- {FILTERS.map(f => ( - ))} +
+ +
+ + +
{/* Table */} diff --git a/core/http/react-ui/src/pages/Manage.jsx b/core/http/react-ui/src/pages/Manage.jsx index 3d24ebea8..bd6bda938 100644 --- a/core/http/react-ui/src/pages/Manage.jsx +++ b/core/http/react-ui/src/pages/Manage.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom' import ResourceMonitor from '../components/ResourceMonitor' import ConfirmDialog from '../components/ConfirmDialog' +import Toggle from '../components/Toggle' import { useModels } from '../hooks/useModels' import { backendControlApi, modelsApi, backendsApi, systemApi, nodesApi } from '../utils/api' @@ -278,6 +279,7 @@ export default function Manage() { + @@ -288,34 +290,49 @@ export default function Manage() { {models.map(model => ( + {/* Enable/Disable toggle */} + + {/* Name */} + {/* Status */} + {/* Backend */} + {/* Use Cases */} + {/* Actions */}
Enabled Name Status Backend
+ handleToggleModel(model.id, model.disabled)} + disabled={togglingModels.has(model.id)} + /> +
- - {model.id} - { e.preventDefault(); navigate(`/app/model-editor/${encodeURIComponent(model.id)}`) }} - style={{ fontSize: '0.75rem', color: 'var(--color-primary)' }} - title="Edit config" - > - - - {!distributedMode && ( + {model.pinned && ( + + )} +
{model.disabled ? ( - + Disabled ) : loadedModelIds.has(model.id) ? ( @@ -328,17 +345,19 @@ export default function Manage() { )} {model.backend || 'Auto'} -
- {/* Stop button - shown when model is loaded */} +
{loadedModelIds.has(model.id) && ( )} - {/* Pin button - prevents model from being unloaded */} - {/* Toggle switch for enabling/disabling model loading on demand */} -
diff --git a/core/http/routes/ui_api.go b/core/http/routes/ui_api.go index a34d22401..f971e9ecb 100644 --- a/core/http/routes/ui_api.go +++ b/core/http/routes/ui_api.go @@ -816,7 +816,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model items = "9" } - backends, err := gallery.AvailableBackends(appConfig.BackendGalleries, appConfig.SystemState) + backends, err := gallery.AvailableBackendsUnfiltered(appConfig.BackendGalleries, appConfig.SystemState) if err != nil { xlog.Error("could not list backends from galleries", "error", err) return c.JSON(http.StatusInternalServerError, map[string]any{ @@ -894,6 +894,12 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model backends = backends.Paginate(pageNum, itemsNum) } + // Get dev suffix from SystemState for development backend detection + devSuffix := "" + if appConfig.SystemState != nil { + devSuffix = appConfig.SystemState.BackendDevSuffix + } + // Convert backends to JSON-friendly format and deduplicate by ID backendsJSON := make([]map[string]any, 0, len(backends)) seenBackendIDs := make(map[string]bool) @@ -920,19 +926,21 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model } backendsJSON = append(backendsJSON, map[string]any{ - "id": backendID, - "name": b.Name, - "description": b.Description, - "icon": b.Icon, - "license": b.License, - "urls": b.URLs, - "tags": b.Tags, - "gallery": b.Gallery.Name, - "installed": b.Installed, - "version": b.Version, - "processing": currentlyProcessing, - "jobID": jobID, - "isDeletion": isDeletionOp, + "id": backendID, + "name": b.Name, + "description": b.Description, + "icon": b.Icon, + "license": b.License, + "urls": b.URLs, + "tags": b.Tags, + "gallery": b.Gallery.Name, + "installed": b.Installed, + "version": b.Version, + "processing": currentlyProcessing, + "jobID": jobID, + "isDeletion": isDeletionOp, + "isMeta": b.IsMeta(), + "isDevelopment": b.IsDevelopment(devSuffix), }) } @@ -974,7 +982,8 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model "totalPages": totalPages, "prevPage": prevPage, "nextPage": nextPage, - "systemCapability": detectedCapability, + "systemCapability": detectedCapability, + "preferDevelopmentBackends": appConfig.PreferDevelopmentBackends, }) }, adminMiddleware)