feat(ai): refine model pools, navigation and GPU deployment configuration (#14257)

This commit is contained in:
ssongliu
2026-10-30 23:00:23 +08:00
committed by GitHub
parent c6329a62db
commit 7156addf79
32 changed files with 347 additions and 148 deletions
+12 -11
View File
@@ -347,17 +347,18 @@ type AgentAccountCreateReq struct {
}
type AgentAccountUpdateReq struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
Models []AgentAccountModel `json:"models"`
}
type AgentAccountVerifyReq struct {
+2 -2
View File
@@ -20,7 +20,7 @@ type IAgentAccountRepo interface {
List(opts ...DBOption) ([]model.AgentAccount, error)
Count() (int64, error)
CountTextByProviders(providers []string) (map[string]int64, error)
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
SaveWithModels(account *model.AgentAccount, models []model.AgentAccountModel) error
}
func NewIAgentAccountRepo() IAgentAccountRepo {
@@ -106,7 +106,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
return counts, nil
}
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
func (a AgentAccountRepo) SaveWithModels(account *model.AgentAccount, models []model.AgentAccountModel) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(account).Error; err != nil {
return err
+25 -2
View File
@@ -1105,9 +1105,23 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
if err != nil {
return err
}
if req.Models != nil {
if account.AppInstallID != 0 {
return buserr.WithDetail("ErrInvalidParams", "local model pool is managed by the deployment", nil)
}
models, err = normalizeAgentAccountModels(account, req.Models)
if err != nil {
return err
}
if err := ensureAccountModelsNotBound(account, models); err != nil {
return err
}
}
requestedVerifyModel := req.VerifyModel
if strings.TrimSpace(requestedVerifyModel) == "" {
requestedVerifyModel = account.VerifyModel
if _, ok := findAgentAccountModelForProvider(provider, models, account.VerifyModel); ok {
requestedVerifyModel = account.VerifyModel
}
}
verifyModel := ""
if account.AppInstallID == 0 || len(models) > 0 {
@@ -1131,7 +1145,16 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.Remark = req.Remark
account.Verified = true
if err := agentAccountRepo.Save(account); err != nil {
if req.Models != nil {
rows := make([]model.AgentAccountModel, 0, len(models))
for _, item := range models {
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
}
err = agentAccountRepo.SaveWithModels(account, rows)
} else {
err = agentAccountRepo.Save(account)
}
if err != nil {
return err
}
terminalai.InvalidateTerminalRuntimeCache()
+1 -1
View File
@@ -88,7 +88,7 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
}
}
if modelsChanged {
err = agentAccountRepo.SaveLocal(account, rows)
err = agentAccountRepo.SaveWithModels(account, rows)
} else if accountChanged {
err = agentAccountRepo.Save(account)
} else {
+64 -58
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -9,6 +10,7 @@ import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/compose-spec/compose-go/v2/interpolation"
"github.com/compose-spec/compose-go/v2/template"
@@ -16,76 +18,72 @@ import (
func LoadModelGPUDevices() dto.ModelGPUDevices {
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
uuidPattern := re.GetRegex(re.NvidiaGPUUUIDPattern)
renderPattern := re.GetRegex(re.IntelGPURenderNamePattern)
ascendPattern := re.GetRegex(re.AscendGPUDeviceNamePattern)
for _, directory := range []string{"/proc/driver/nvidia/gpus", "/sys/class/drm", "/dev"} {
entries, err := os.ReadDir(directory)
if err != nil {
if !os.IsNotExist(err) {
result.Warnings = append(result.Warnings, err.Error())
}
ok, client := accelerator.New()
if !ok {
return result
}
snapshot, err := client.Collect(context.Background())
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
return result
}
result.Warnings = append(result.Warnings, snapshot.Info.Warnings...)
for _, monitored := range snapshot.Devices {
if monitored.ParentID != "" {
continue
}
for _, entry := range entries {
var device dto.ModelGPUDevice
var devicePath string
switch directory {
case "/proc/driver/nvidia/gpus":
informationPath := filepath.Join(directory, entry.Name(), "information")
content, err := os.ReadFile(informationPath)
device := dto.ModelGPUDevice{Available: true}
devicePath := ""
switch {
case monitored.GPU != nil:
if monitored.Vendor == "nvidia" && !re.GetRegex(re.NvidiaGPUUUIDPattern).MatchString(monitored.GPU.UUID) {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID from nvidia-smi: %s", monitored.GPU.UUID))
continue
}
device.ID = monitored.ID
device.Name = fmt.Sprintf("GPU %d · %s", monitored.Index, monitored.Name)
device.Type = monitored.Vendor
device.UUID = monitored.GPU.UUID
case monitored.XPU != nil:
devicePath = monitored.XPU.Basic.DrmDevice
if !re.GetRegex(re.IntelGPUDevicePathPattern).MatchString(devicePath) {
directory := filepath.Join("/sys/bus/pci/devices", filepath.Base(monitored.BusID), "drm")
entries, err := os.ReadDir(directory)
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
result.Warnings = append(result.Warnings, fmt.Sprintf("resolve Intel GPU %d render device: %v", monitored.Index, err))
continue
}
fields := map[string]string{}
for _, line := range strings.Split(string(content), "\n") {
key, value, found := strings.Cut(line, ":")
if found {
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
devicePath = ""
for _, entry := range entries {
if re.GetRegex(re.IntelGPURenderNamePattern).MatchString(entry.Name()) {
devicePath = filepath.Join("/dev/dri", entry.Name())
break
}
}
uuid := fields["GPU UUID"]
minor, err := strconv.Atoi(fields["Device Minor"])
if !uuidPattern.MatchString(uuid) || err != nil || minor < 0 {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID or device minor in %s", informationPath))
if devicePath == "" {
result.Warnings = append(result.Warnings, fmt.Sprintf("Intel GPU %d has no render device", monitored.Index))
continue
}
name := fields["Model"]
if name == "" {
name = "NVIDIA"
}
device = dto.ModelGPUDevice{ID: "nvidia:" + uuid, Name: name + " · " + entry.Name(), Type: "nvidia", UUID: uuid}
devicePath = filepath.Join("/dev", "nvidia"+strconv.Itoa(minor))
case "/sys/class/drm":
if !renderPattern.MatchString(entry.Name()) {
continue
}
vendor, err := os.ReadFile(filepath.Join(directory, entry.Name(), "device/vendor"))
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
if strings.TrimSpace(string(vendor)) != "0x8086" {
continue
}
devicePath = filepath.Join("/dev/dri", entry.Name())
device = dto.ModelGPUDevice{ID: "intel:" + devicePath, Name: "Intel · " + entry.Name(), Type: "intel"}
case "/dev":
if !ascendPattern.MatchString(entry.Name()) {
continue
}
devicePath = filepath.Join(directory, entry.Name())
device = dto.ModelGPUDevice{ID: "ascend:" + strings.TrimPrefix(entry.Name(), "davinci"), Name: "Ascend · " + entry.Name(), Type: "ascend"}
}
device.ID = "intel:" + devicePath
device.Name = fmt.Sprintf("XPU %d · %s", monitored.Index, monitored.Name)
device.Type = "intel"
case monitored.NPU != nil:
device.ID = fmt.Sprintf("ascend:%d", monitored.Index)
device.Name = fmt.Sprintf("NPU %d · Chip %d · %s", monitored.NPUIndex, monitored.ChipIndex, monitored.Name)
device.Type = "ascend"
devicePath = fmt.Sprintf("/dev/davinci%d", monitored.Index)
default:
continue
}
if devicePath != "" {
info, err := os.Stat(devicePath)
device.Available = err == nil && info.Mode()&os.ModeCharDevice != 0
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
} else {
device.Available = info.Mode()&os.ModeCharDevice != 0
}
result.Devices = append(result.Devices, device)
}
result.Devices = append(result.Devices, device)
}
return result
}
@@ -649,9 +647,17 @@ func resolveModelGPUValue(value string, params map[string]interface{}) (string,
func loadModelAscendDeviceIDs() []string {
ids := []string{}
for _, device := range LoadModelGPUDevices().Devices {
if device.Type == "ascend" && device.Available {
ids = append(ids, strings.TrimPrefix(device.ID, "ascend:"))
entries, err := os.ReadDir("/dev")
if err != nil {
return ids
}
for _, entry := range entries {
if !re.GetRegex(re.AscendGPUDeviceNamePattern).MatchString(entry.Name()) {
continue
}
info, err := os.Stat(filepath.Join("/dev", entry.Name()))
if err == nil && info.Mode()&os.ModeCharDevice != 0 {
ids = append(ids, strings.TrimPrefix(entry.Name(), "davinci"))
}
}
slices.SortFunc(ids, func(a, b string) int {
+2 -1
View File
@@ -72,7 +72,8 @@ type Device struct {
}
type Basic struct {
UUID string `json:"uuid"`
DrmDevice string `json:"-"`
UUID string `json:"uuid"`
DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"`
+4
View File
@@ -93,6 +93,7 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
xpu := Device{
ProcessStatus: "unavailable",
Basic: Basic{
DrmDevice: device.DrmDevice,
UUID: device.UUID,
DeviceID: device.DeviceID,
DeviceName: device.DeviceName,
@@ -137,6 +138,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
return
}
if info.DrmDevice != "" {
xpu.Basic.DrmDevice = info.DrmDevice
}
xpu.Basic.DriverVersion = info.DriverVersion
bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64)
+1
View File
@@ -71,6 +71,7 @@ var WebUrlMap = map[string]struct{}{
"/ai/skills-hub": {},
"/ai/skills-hub/targets": {},
"/ai/benchmark": {},
"/ai/model": {},
"/ai/model/account": {},
"/ai/model/local": {},
"/ai/model/local/templates": {},
+1
View File
@@ -737,6 +737,7 @@ export namespace AI {
baseURL: string;
apiType: string;
authMode: string;
models?: AgentAccountModel[];
verifyModel: string;
validateAvailability: boolean;
remark: string;
@@ -92,12 +92,15 @@ const expiresInfo = ref(0);
const expiresAlertVisible = ref(false);
const expiresAlertKey = computed(() => (isEnterprise.value ? 'xpack.expiresEnterpriseAlert' : 'xpack.expiresProAlert'));
const handleChange = (label: string) => {
const handleChange = async (label: string) => {
const btn = buttonArray.value.find((btn) => btn.label === label);
if (!btn) return;
if (btn.path) routerToPathWithQuery(btn.path, { uncached: 'true' });
else if (btn.name) routerToNameWithQuery(btn.name, { uncached: 'true' });
activeName.value = btn.label;
try {
if (btn.path) await routerToPathWithQuery(btn.path, { uncached: 'true' });
else if (btn.name) await routerToNameWithQuery(btn.name, { uncached: 'true' });
} finally {
syncActiveName();
}
};
onMounted(() => {
@@ -154,7 +157,9 @@ function syncActiveName() {
if (buttonArray.value.length) {
let isPathExist = false;
const btn = buttonArray.value.find((btn) => {
return btn.path && router.currentRoute.value.path.startsWith(btn.path);
return btn.path
? router.currentRoute.value.path.startsWith(btn.path)
: btn.name && router.currentRoute.value.name === btn.name;
});
if (btn) {
isPathExist = true;
+7 -2
View File
@@ -1092,14 +1092,19 @@ const message = {
container_conn_helper: 'Use this address for inter-container access or connection',
from_remote: 'This model was not downloaded via 1Panel, no related pull logs.',
no_logs: 'The pull logs for this model have been deleted and cannot be viewed.',
vllmVersionHelper: 'For FusionXpark GB10 servers, please select the -cu130 version.',
vllmVersionHelper: {
default: 'Official standard version with CUDA 13.0, for newer drivers and newer GPU architectures.',
cuda: 'Legacy architecture compatibility version with CUDA 12.9, for older GPU architectures using CUDA versions below 13.0.',
ubuntu: 'NVIDIA GB10 standard version with CUDA 13.0, for NVIDIA GB10 devices.',
dspark: 'NVIDIA GB10 dual-device custom version, using a custom image maintained by 1Panel.',
},
vllmDeployProfile: 'Deployment model',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'The startup command must use port {0}; otherwise, the service will be inaccessible.',
ascendVisibleDevices: 'Ascend visible devices (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Sync to model account',
syncModelAccount: 'Sync to model service',
modelAccountAddressHelper:
'Container address is for agent containers; 127.0.0.1, system IP, or custom address is for AI Gateway or external services.',
containerAddress: 'Container address',
+8 -2
View File
@@ -1094,14 +1094,20 @@ const message = {
container_conn_helper: 'Utilice esta dirección para el acceso o conexión entre contenedores',
from_remote: 'Este modelo no fue descargado vía 1Panel, no hay registros de descarga relacionados.',
no_logs: 'Los registros de descarga de este modelo han sido eliminados y no se pueden consultar.',
vllmVersionHelper: 'Para servidores FusionXpark GB10, seleccione la versión -cu130.',
vllmVersionHelper: {
default:
'Versión oficial estándar con CUDA 13.0, para controladores recientes y GPU de nuevas arquitecturas.',
cuda: 'Versión compatible con arquitecturas antiguas con CUDA 12.9, para GPU antiguas que utilizan versiones de CUDA anteriores a 13.0.',
ubuntu: 'Versión estándar de NVIDIA GB10 con CUDA 13.0, para dispositivos NVIDIA GB10.',
dspark: 'Versión personalizada de NVIDIA GB10 para dos dispositivos, con una imagen mantenida por 1Panel.',
},
vllmDeployProfile: 'Modelo de despliegue',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'El comando de inicio debe usar el puerto {0}; de lo contrario, no se podrá acceder al servicio.',
ascendVisibleDevices: 'Dispositivos Ascend visibles (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Sincronizar con cuenta de modelo',
syncModelAccount: 'Sincronizar con servicio de modelo',
modelAccountAddressHelper:
'La dirección del contenedor es para contenedores de agentes; 127.0.0.1, IP del sistema o dirección personalizada es para AI Gateway o servicios externos.',
containerAddress: 'Dirección del contenedor',
+7 -2
View File
@@ -1076,14 +1076,19 @@ const message = {
container_conn_helper: 'از این آدرس برای دسترسی بین کانتینرها یا اتصال استفاده کنید',
from_remote: 'این مدل از طریق 1Panel دانلود نشده است، لاگ مربوط به دریافت وجود ندارد.',
no_logs: 'لاگ دریافت این مدل حذف شده است و قابل مشاهده نیست.',
vllmVersionHelper: 'برای سرورهای FusionXpark GB10، لطفاً نسخه -cu130 را انتخاب کنید.',
vllmVersionHelper: {
default: 'نسخه استاندارد رسمی با CUDA 13.0، مناسب درایورهای جدید و معماری‌های جدید GPU.',
cuda: 'نسخه سازگار با معماری‌های قدیمی با CUDA 12.9، مناسب GPUهای قدیمی که از نسخه‌های CUDA پایین‌تر از 13.0 استفاده می‌کنند.',
ubuntu: 'نسخه استاندارد NVIDIA GB10 با CUDA 13.0، مناسب دستگاه‌های NVIDIA GB10.',
dspark: 'نسخه سفارشی NVIDIA GB10 برای دو دستگاه، با ایمیج سفارشی که توسط 1Panel نگهداری می‌شود.',
},
vllmDeployProfile: 'مدل استقرار',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'فرمان راه‌اندازی باید از پورت {0} استفاده کند؛ در غیر این صورت سرویس قابل دسترسی نخواهد بود.',
ascendVisibleDevices: 'دستگاه‌های قابل مشاهده Ascend (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'همگام‌سازی با حساب مدل',
syncModelAccount: 'همگام‌سازی با سرویس مدل',
modelAccountAddressHelper:
'آدرس کانتینر برای کانتینرهای عامل است؛ 127.0.0.1، IP سیستم یا آدرس سفارشی برای دروازه AI یا خدمات خارجی است.',
containerAddress: 'آدرس کانتینر',
+8 -2
View File
@@ -1080,14 +1080,20 @@ const message = {
container_conn_helper: 'コンテナ間のアクセスまたは接続にこのアドレスを使用',
from_remote: 'このモデルは1Panelを介してダウンロードされておらず、関連するプルログはありません。',
no_logs: 'このモデルのプルログは削除されており、関連するログを表示できません。',
vllmVersionHelper: 'FusionXpark GB10 サーバーでは -cu130 バージョンを選択してください。',
vllmVersionHelper: {
default:
'公式の標準バージョン。CUDA 13.0 を使用し、新しいドライバーと新しい GPU アーキテクチャに対応します。',
cuda: '旧アーキテクチャ向け互換バージョン。CUDA 12.9 を使用し、CUDA 13.0 未満を使用する旧世代 GPU に対応します。',
ubuntu: 'NVIDIA GB10 標準版。CUDA 13.0 を使用し、NVIDIA GB10 デバイスに対応します。',
dspark: 'NVIDIA GB10 の 2 台構成向けカスタム版。1Panel が独自に保守するカスタムイメージです。',
},
vllmDeployProfile: 'デプロイモデル',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'起動コマンドではポート {0} を使用する必要があります。使用しない場合、サービスにアクセスできません。',
ascendVisibleDevices: 'Ascend 可視デバイス(ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'モデルアカウントに同期',
syncModelAccount: 'モデルサービスに同期',
modelAccountAddressHelper:
'コンテナアドレスはエージェントコンテナ向けです。127.0.0.1、システム IP、カスタムアドレスは AI Gateway または外部サービス向けです。',
containerAddress: 'コンテナアドレス',
+7 -2
View File
@@ -1070,13 +1070,18 @@ const message = {
container_conn_helper: '컨테이너 간 접근 또는 연결에 이 주소를 사용',
from_remote: '이 모델은 1Panel을 통해 다운로드되지 않았으며 관련 풀 로그가 없습니다.',
no_logs: '이 모델의 풀 로그가 삭제되어 관련 로그를 볼 수 없습니다.',
vllmVersionHelper: 'FusionXpark GB10 서버는 -cu130 버전을 선택하세요.',
vllmVersionHelper: {
default: '공식 표준 버전으로, CUDA 13.0을 사용하며 최신 드라이버와 새로운 GPU 아키텍처에 적합합니다.',
cuda: '이전 아키텍처 호환 버전으로, CUDA 12.9를 사용하며 CUDA 13.0 미만을 사용하는 이전 GPU 아키텍처에 적합합니다.',
ubuntu: 'NVIDIA GB10 표준 버전으로, CUDA 13.0을 사용하며 NVIDIA GB10 장치에 적합합니다.',
dspark: 'NVIDIA GB10 장치 2대 구성용 맞춤 버전으로, 1Panel이 자체 유지 관리하는 맞춤 이미지입니다.',
},
vllmDeployProfile: '배포 모델',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: '시작 명령은 {0} 포트를 사용해야 하며, 그렇지 않으면 서비스에 접근할 수 없습니다.',
ascendVisibleDevices: 'Ascend 표시 장치 (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: '모델 계정에 동기화',
syncModelAccount: '모델 서비스에 동기화',
modelAccountAddressHelper:
'컨테이너 주소는 에이전트 컨테이너 호출에 적합합니다. 127.0.0.1, 시스템 IP 또는 사용자 지정 주소는 AI Gateway 또는 외부 서비스 호출에 적합합니다.',
containerAddress: '컨테이너 주소',
+7 -2
View File
@@ -1067,13 +1067,18 @@ const message = {
container_conn_helper: 'ໃຊ້ທີ່ຢູ່ນີ້ສຳລັບການເຂົ້າເຖິງ ຫຼື ເຊື່ອມຕໍ່ລະຫວ່າງຄອນເທນເນີ',
from_remote: 'ໂມເດວນີ້ບໍ່ໄດ້ດາວໂຫຼດຜ່ານ 1Panel, ບໍ່ມີລັອກການດຶງຂໍ້ມູນທີ່ກ່ຽວຂ້ອງ.',
no_logs: 'ລັອກການດຶງຂໍ້ມູນຂອງໂມເດວນີ້ຖືກລຶບແລ້ວ ແລະ ບໍ່ສາມາດເບິ່ງໄດ້.',
vllmVersionHelper: 'ສຳລັບເຊີເວີ FusionXpark GB10, ກະລຸນາເລືອກເວີຊັນ -cu130.',
vllmVersionHelper: {
default: 'ເວີຊັນມາດຕະຖານທາງການທີ່ໃຊ້ CUDA 13.0, ເໝາະສຳລັບໄດຣເວີໃໝ່ ແລະ GPU ສະຖາປັດຕະຍະກຳໃໝ່.',
cuda: 'ເວີຊັນຮອງຮັບສະຖາປັດຕະຍະກຳເກົ່າທີ່ໃຊ້ CUDA 12.9, ເໝາະສຳລັບ GPU ເກົ່າທີ່ໃຊ້ CUDA ຕ່ຳກວ່າ 13.0.',
ubuntu: 'ເວີຊັນມາດຕະຖານ NVIDIA GB10 ທີ່ໃຊ້ CUDA 13.0, ເໝາະສຳລັບອຸປະກອນ NVIDIA GB10.',
dspark: 'ເວີຊັນປັບແຕ່ງ NVIDIA GB10 ສຳລັບສອງອຸປະກອນ, ໃຊ້ອິມເມຈປັບແຕ່ງທີ່ 1Panel ດູແລເອງ.',
},
vllmDeployProfile: 'ໂມເດວສຳລັບຕິດຕັ້ງ',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: 'ຄຳສັ່ງເລີ່ມຕົ້ນຕ້ອງໃຊ້ພອດ {0}; ບໍ່ດັ່ງນັ້ນຈະບໍ່ສາມາດເຂົ້າເຖິງບໍລິການໄດ້.',
ascendVisibleDevices: 'ອຸປະກອນ Ascend ທີ່ເຫັນໄດ້ (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'ຊິ້ງຄ໌ໄປຍັງບັນຊີໂມເດວ',
syncModelAccount: 'ຊິ້ງຄ໌ໄປຍັງບໍລິການໂມເດວ',
modelAccountAddressHelper:
'ທີ່ຢູ່ຄອນເທນເນີແມ່ນສຳລັບຄອນເທນເນີຕົວແທນ; 127.0.0.1, IP ລະບົບ, ຫຼື ທີ່ຢູ່ກຳນົດເອງແມ່ນສຳລັບ AI Gateway ຫຼື ບໍລິການພາຍນອກ.',
containerAddress: 'ທີ່ຢູ່ຄອນເທນເນີ',
+7 -2
View File
@@ -1102,14 +1102,19 @@ const message = {
container_conn_helper: 'Gunakan alamat ini untuk akses atau sambungan antara kontena',
from_remote: 'Model ini tidak dimuat turun melalui 1Panel; tiada log muat turun berkaitan.',
no_logs: 'Log muat turun model ini telah dipadam dan tidak boleh dilihat.',
vllmVersionHelper: 'Untuk pelayan FusionXpark GB10, sila pilih versi -cu130.',
vllmVersionHelper: {
default: 'Versi standard rasmi dengan CUDA 13.0, untuk pemacu baharu dan seni bina GPU baharu.',
cuda: 'Versi keserasian seni bina lama dengan CUDA 12.9, untuk GPU lama yang menggunakan versi CUDA di bawah 13.0.',
ubuntu: 'Versi standard NVIDIA GB10 dengan CUDA 13.0, untuk peranti NVIDIA GB10.',
dspark: 'Versi tersuai NVIDIA GB10 untuk dua peranti, menggunakan imej tersuai yang diselenggara oleh 1Panel.',
},
vllmDeployProfile: 'Model penggunaan',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'Perintah permulaan mesti menggunakan port {0}; jika tidak, perkhidmatan tidak dapat diakses.',
ascendVisibleDevices: 'Peranti Ascend boleh dilihat (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Segerakkan ke akaun model',
syncModelAccount: 'Segerakkan ke perkhidmatan model',
modelAccountAddressHelper:
'Alamat kontena sesuai untuk kontena agen; 127.0.0.1, IP sistem, atau alamat tersuai sesuai untuk AI Gateway atau perkhidmatan luaran.',
containerAddress: 'Alamat kontena',
+7 -2
View File
@@ -1098,14 +1098,19 @@ const message = {
container_conn_helper: 'Use este endereço para acesso ou conexão entre contêineres',
from_remote: 'Este modelo não foi baixado pelo 1Panel; não há logs de download relacionados.',
no_logs: 'Os logs de download deste modelo foram excluídos e não podem ser visualizados.',
vllmVersionHelper: 'Para servidores FusionXpark GB10, selecione a versão -cu130.',
vllmVersionHelper: {
default: 'Versão padrão oficial com CUDA 13.0, para drivers recentes e GPUs de novas arquiteturas.',
cuda: 'Versão de compatibilidade com arquiteturas antigas com CUDA 12.9, para GPUs antigas que usam versões do CUDA inferiores à 13.0.',
ubuntu: 'Versão padrão do NVIDIA GB10 com CUDA 13.0, para dispositivos NVIDIA GB10.',
dspark: 'Versão personalizada do NVIDIA GB10 para dois dispositivos, com uma imagem mantida pelo 1Panel.',
},
vllmDeployProfile: 'Modelo de implantação',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'O comando de inicialização deve usar a porta {0}; caso contrário, o serviço ficará inacessível.',
ascendVisibleDevices: 'Dispositivos Ascend visíveis (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Sincronizar com conta de modelo',
syncModelAccount: 'Sincronizar com serviço de modelo',
modelAccountAddressHelper:
'Use o endereço do contêiner para agentes; 127.0.0.1, IP do sistema ou endereço personalizado para AI Gateway ou serviços externos.',
containerAddress: 'Endereço do contêiner',
+8 -2
View File
@@ -1091,13 +1091,19 @@ const message = {
container_conn_helper: 'Используйте этот адрес для доступа или подключения между контейнерами',
from_remote: 'Эта модель не была загружена через 1Panel, нет связанных журналов извлечения.',
no_logs: 'Журналы извлечения для этой модели были удалены и не могут быть просмотрены.',
vllmVersionHelper: 'Для серверов FusionXpark GB10 выберите версию -cu130.',
vllmVersionHelper: {
default:
'Официальная стандартная версия с CUDA 13.0 для новых драйверов и видеокарт с новой архитектурой.',
cuda: 'Версия совместимости со старыми архитектурами с CUDA 12.9 для видеокарт, использующих версии CUDA ниже 13.0.',
ubuntu: 'Стандартная версия NVIDIA GB10 с CUDA 13.0 для устройств NVIDIA GB10.',
dspark: 'Специальная версия NVIDIA GB10 для двух устройств с образом, поддерживаемым 1Panel.',
},
vllmDeployProfile: 'Модель развертывания',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: 'Команда запуска должна использовать порт {0}, иначе сервис будет недоступен.',
ascendVisibleDevices: 'Видимые устройства Ascend (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Синхронизировать с аккаунтом модели',
syncModelAccount: 'Синхронизировать с сервисом модели',
modelAccountAddressHelper:
'Адрес контейнера подходит для контейнеров агентов; 127.0.0.1, системный IP или пользовательский адрес подходит для AI Gateway или внешних сервисов.',
containerAddress: 'Адрес контейнера',
+7 -2
View File
@@ -1101,14 +1101,19 @@ const message = {
container_conn_helper: 'Konteynerler arası erişim veya bağlantı için bu adresi kullanın',
from_remote: 'Bu model 1Panel aracılığıyla indirilmedi, ilgili çekme logları yok.',
no_logs: 'Bu modelin çekme logları silindi ve görüntülenemiyor.',
vllmVersionHelper: 'FusionXpark GB10 sunucuları için lütfen -cu130 sürümünü seçin.',
vllmVersionHelper: {
default: 'Yeni sürücüler ve yeni GPU mimarileri için CUDA 13.0 içeren resmi standart sürüm.',
cuda: '13.0 altındaki CUDA sürümlerini kullanan eski GPU mimarileri için CUDA 12.9 içeren uyumluluk sürümü.',
ubuntu: 'NVIDIA GB10 cihazları için CUDA 13.0 içeren NVIDIA GB10 standart sürümü.',
dspark: '1Panel tarafından bakımı yapılan özel imajı kullanan, iki cihaz için NVIDIA GB10 özel sürümü.',
},
vllmDeployProfile: 'Dağıtım modeli',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper:
'Başlatma komutu {0} numaralı bağlantı noktasını kullanmalıdır; aksi halde hizmete erişilemez.',
ascendVisibleDevices: 'Görünür Ascend cihazları (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Model hesabına senkronize et',
syncModelAccount: 'Model hizmetine senkronize et',
modelAccountAddressHelper:
'Konteyner adresi ajan konteynerleri içindir; 127.0.0.1, sistem IP veya özel adres AI Gateway ya da harici servisler içindir.',
containerAddress: 'Konteyner adresi',
+7 -2
View File
@@ -1030,13 +1030,18 @@ const message = {
container_conn_helper: '容器間瀏覽或連接使用此地址',
from_remote: '該模型並非透過 1Panel 下載,無相關拉取日誌。',
no_logs: '該模型的拉取日誌已被刪除,無法檢視相關日誌。',
vllmVersionHelper: 'FusionXpark GB10 伺服器請選擇 -cu130 版本',
vllmVersionHelper: {
default: '官方標準版本,CUDA 13.0,適用於新驅動程式和新架構顯示卡。',
cuda: '舊架構相容版本,CUDA 12.9,適用於 CUDA 版本低於 13.0 的舊架構顯示卡。',
ubuntu: 'NVIDIA GB10 標準版,CUDA 13.0,適用於 NVIDIA GB10 裝置。',
dspark: 'NVIDIA GB10 雙裝置客製版,由 1Panel 自行維護的客製映像。',
},
vllmDeployProfile: '部署模型',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: '啟動命令必須使用 {0} 連接埠,否則服務將無法存取。',
ascendVisibleDevices: 'Ascend 可見裝置(ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: '同步到模型帳號',
syncModelAccount: '同步到模型服務',
modelAccountAddressHelper:
'容器地址適合智能體容器呼叫;127.0.0.1、本機 IP 或自訂地址適合 AI 閘道或外部服務呼叫。',
containerAddress: '容器地址',
+7 -2
View File
@@ -1043,13 +1043,18 @@ const message = {
container_conn_helper: '容器间访问或连接使用此地址',
from_remote: '该模型并非通过 1Panel 下载,无相关拉取日志。',
no_logs: '该模型的拉取日志已被删除,无法查看相关日志。',
vllmVersionHelper: 'FusionXpark GB10 服务器请选择 -cu130 版本',
vllmVersionHelper: {
default: '官方标准版本,CUDA 13.0,适用于新驱动和新架构显卡。',
cuda: '旧架构兼容版本,CUDA 12.9,适用于 CUDA 版本低于 13.0 的旧架构显卡。',
ubuntu: 'NVIDIA GB10 标准版,CUDA 13.0,适用于 NVIDIA GB10 设备。',
dspark: 'NVIDIA GB10 双设备定制版,1Panel 自行维护的定制镜像。',
},
vllmDeployProfile: '部署模型',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: '启动命令必须使用 {0} 端口,否则服务将无法访问。',
ascendVisibleDevices: 'Ascend 可见设备(ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: '同步到模型账号',
syncModelAccount: '同步到模型服务',
modelAccountAddressHelper:
'容器地址适合智能体容器调用;127.0.0.1、本机 IP 或自定义地址适合 AI 网关或外部服务调用。',
containerAddress: '容器地址',
+26 -2
View File
@@ -31,8 +31,15 @@ const clearLoginStatus = () => {
};
router.beforeEach(async (to, from) => {
const { entrance, isEnterprise, isEnterpriseLicenseLoaded, isEnterpriseLicensed, isLogin, isOnRestart } =
useGlobalStore();
const {
entrance,
isEnterprise,
isEnterpriseLicenseLoaded,
isEnterpriseLicensed,
isProductPro,
isLogin,
isOnRestart,
} = useGlobalStore();
NProgress.start();
axiosCanceler.removeAllPending();
@@ -133,6 +140,23 @@ router.beforeEach(async (to, from) => {
removePanelCache('tabs', activeMenuKey);
}
if (to.name === 'ModelFiles') {
if (!isEnterprise.value) {
try {
await loadProductProFromDB();
} catch (error) {
console.warn('Failed to refresh model marketplace license status', error);
}
}
if (!isProductPro.value) {
if (cachedRoute === to.path) {
removePanelCache('tabs', activeMenuKey);
}
NProgress.done();
return { path: '/ai/model/account', query: to.query, replace: true };
}
}
if (!hasRouteAccess(to)) {
MsgError(i18n.global.t('commons.res.forbidden'));
NProgress.done();
+27 -15
View File
@@ -6,30 +6,20 @@ const aiRouter = {
path: '/ai',
name: 'AI-Menu',
component: Layout,
redirect: '/ai/model/account',
redirect: '/ai/model',
meta: {
icon: 'p-jiqiren2',
title: 'menu.aiTools',
},
children: [
{
path: '/ai/model/account',
path: '/ai/model',
name: 'AIModel',
component: () => import('@/views/ai/model/index.vue'),
redirect: '/ai/model/files',
meta: {
icon: 'p-moxing-menu',
title: 'aiTools.model.model',
permission: 'ai_model_view',
},
},
{
path: '/ai/model/local',
hidden: true,
name: 'LocalModel',
component: () => import('@/views/ai/model/index.vue'),
meta: {
title: 'aiTools.model.deployments',
activeMenu: '/ai/model/account',
activeMenu: '/ai/model',
permission: 'ai_model_view',
},
},
@@ -40,7 +30,29 @@ const aiRouter = {
component: () => import('@/views/ai/model/index.vue'),
meta: {
title: 'aiTools.model.files',
activeMenu: '/ai/model/account',
activeMenu: '/ai/model',
permission: 'ai_model_view',
},
},
{
path: '/ai/model/account',
hidden: true,
name: 'ModelServices',
component: () => import('@/views/ai/model/index.vue'),
meta: {
title: 'aiTools.model.services',
activeMenu: '/ai/model',
permission: 'ai_model_view',
},
},
{
path: '/ai/model/local',
hidden: true,
name: 'LocalModel',
component: () => import('@/views/ai/model/index.vue'),
meta: {
title: 'aiTools.model.deployments',
activeMenu: '/ai/model',
permission: 'ai_model_view',
},
},
@@ -91,7 +91,7 @@
<div class="model-discovery">
<div class="model-discovery__header">
<el-radio-group v-model="modelConfigMode" size="small" @change="handleModelConfigModeChange">
<el-radio-button value="discover">
<el-radio-button v-if="supportsModelDiscovery || defaultModels.length" value="discover">
{{
$t(
supportsModelDiscovery
@@ -284,6 +284,7 @@ const { isIntl } = useGlobalStore();
const form = reactive({
id: 0,
appInstallId: 0,
provider: 'custom',
name: '',
baseURL: '',
@@ -308,14 +309,16 @@ const selectedAPIConfig = computed(() =>
);
const isImageAPIType = computed(() => form.apiType.endsWith('-images'));
const isEmbeddingAPIType = computed(() => form.apiType === 'openai-embeddings');
const supportsModelDiscovery = computed(() => !form.id && Boolean(selectedAPIConfig.value?.supportsModelDiscovery));
const supportsModelDiscovery = computed(() => Boolean(selectedAPIConfig.value?.supportsModelDiscovery));
const defaultModels = computed(() => {
if (selectedAPIConfig.value?.models.length) {
return selectedAPIConfig.value.models;
}
return isImageAPIType.value ? [] : selectedProvider.value?.models || [];
});
const showModelDiscovery = computed(() => !form.id && (supportsModelDiscovery.value || defaultModels.value.length > 0));
const showModelDiscovery = computed(
() => !form.appInstallId && (Boolean(form.id) || supportsModelDiscovery.value || defaultModels.value.length > 0),
);
const showInitialModel = computed(
() =>
!form.id &&
@@ -330,7 +333,8 @@ const showVerifyModelSelect = computed(
() =>
requiresVerifyModel.value &&
verifyModelOptions.value.length > 0 &&
(Boolean(form.id) || (!showModelDiscovery.value && !showInitialModel.value)),
!showModelDiscovery.value &&
!showInitialModel.value,
);
const apiTypeOptions = computed(() => selectedProvider.value?.apiTypes.map((item) => item.apiType) || []);
const authModeOptions = computed(() => selectedAPIConfig.value?.authModes || []);
@@ -580,20 +584,18 @@ const submit = async () => {
return;
}
let accountModels = initialModel ? [initialModel] : [];
if (!form.id) {
if (showModelDiscovery.value && modelConfigMode.value === 'manual') {
const models = normalizeManualModels();
if (!models) {
return;
}
accountModels = models;
} else if (showModelDiscovery.value) {
accountModels = discoveredModels.value;
}
if (accountModels.length === 0) {
MsgError(i18n.global.t('aiTools.agents.accountModelsRequired'));
if (showModelDiscovery.value && modelConfigMode.value === 'manual') {
const models = normalizeManualModels();
if (!models) {
return;
}
accountModels = models;
} else if (showModelDiscovery.value) {
accountModels = discoveredModels.value;
}
if ((!form.id || showModelDiscovery.value) && accountModels.length === 0) {
MsgError(i18n.global.t('aiTools.agents.accountModelsRequired'));
return;
}
let verifyModel = '';
if (requiresVerifyModel.value) {
@@ -623,6 +625,7 @@ const submit = async () => {
validateAvailability: form.validateAvailability,
remark: form.remark,
syncAgents: form.syncAgents,
models: showModelDiscovery.value ? accountModels : undefined,
});
} else {
await createAgentAccount({
@@ -653,6 +656,7 @@ const handleClose = () => {
formRef.value?.resetFields();
loading.value = false;
form.id = 0;
form.appInstallId = 0;
form.provider = 'custom';
form.name = '';
form.baseURL = '';
@@ -671,6 +675,7 @@ const handleClose = () => {
interface OpenParams {
id?: number;
appInstallId?: number;
provider?: string;
name?: string;
baseURL?: string;
@@ -691,6 +696,7 @@ const openDrawer = async (params?: OpenParams) => {
}
if (params?.id) {
form.id = params.id;
form.appInstallId = params.appInstallId || 0;
form.provider = params.provider || '';
form.name = params.name || '';
form.baseURL = params.baseURL || '';
@@ -703,12 +709,19 @@ const openDrawer = async (params?: OpenParams) => {
? String(params.apiType)
: provider?.defaultApiType || '';
form.authMode = params.authMode || selectedAPIConfig.value?.defaultAuthMode || '';
resetModelDiscovery();
modelConfigMode.value = 'manual';
manualModels.value = editModels.value.length
? editModels.value.map((item) => ({ ...item }))
: [buildInitialModel()];
form.verifyModel = params.verifyModel || editModels.value[0]?.id || '';
manualVerifyIndex.value = manualModels.value.findIndex((item) => item.id === form.verifyModel);
form.remark = params.remark || '';
form.syncAgents = false;
return;
}
form.id = 0;
form.appInstallId = 0;
form.name = '';
form.baseURL = '';
form.apiKey = '';
@@ -773,7 +786,7 @@ const handleAPITypeChange = () => {
};
watch([() => form.apiKey, () => form.baseURL], () => {
if (showModelDiscovery.value) {
if (!form.id && showModelDiscovery.value) {
loadDefaultModels();
}
});
@@ -13,7 +13,12 @@
<template v-else-if="visible && instance && !loading && view !== VllmPlaceholder">
<div v-if="instance.engine !== 'ollama'" class="service-card">
<div class="flex flex-wrap gap-3 items-center mb-4">
<strong>{{ engineLabels[instance.engine] }} {{ instance.version }}</strong>
<strong>
{{ engineLabels[instance.engine] }}
{{
instance.engine === 'vllm' ? formatVllmVersion(instance.version) : instance.version
}}
</strong>
<Status :status="instance.status" />
</div>
<div class="service-actions">
@@ -110,7 +115,7 @@ import TensorForm from '../../tensorrt/operate/index.vue';
import VllmPlaceholder from '../../vllm/index.vue';
import ContainerLogs from '@/components/log/container-drawer/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { readPages, loadVllmAPI, vllmAvailable, type Deployment } from '../../deployment';
import { formatVllmVersion, readPages, loadVllmAPI, vllmAvailable, type Deployment } from '../../deployment';
const emit = defineEmits(['search', 'upgrade']);
const { isMobile, currentNode, isProductPro, isFxplay } = useGlobalStore();
@@ -1,5 +1,5 @@
<template>
<details open class="form-section">
<details ref="sectionRef" open class="form-section">
<summary class="form-section-header">
<span>{{ $t('aiTools.model.gpuAcceleration') }}</span>
<el-switch
@@ -126,6 +126,7 @@ import i18n from '@/lang';
const gpuTypeLabels: Record<string, string> = { nvidia: 'NVIDIA', intel: 'Intel', ascend: 'Ascend', amd: 'AMD' };
const props = withDefaults(defineProps<{ imageType?: string }>(), { imageType: 'nvidia' });
const model = defineModel<AI.ModelGPUConfig>({ required: true });
const sectionRef = ref<HTMLDetailsElement>();
const loading = ref(false);
const gpuWarnings = ref<string[]>([]);
const gpuDevices = ref<AI.ModelGPUDevice[]>();
@@ -200,6 +201,9 @@ const changeMode = () => {
const changeEnabled = () => {
model.value.error = undefined;
if (model.value.enabled && sectionRef.value) {
sectionRef.value.open = true;
}
if (!model.value.enabled) {
model.value.mode = 'all';
model.value.count = undefined;
@@ -102,7 +102,7 @@ const open = async (row: Deployment) => {
MsgError(i18n.global.t('commons.res.notFound'));
return;
}
const hostMode = item.appVersion.toLowerCase().startsWith('nvidia-gb10-dspark-');
const hostMode = item.appVersion.trim().toLowerCase().startsWith('nvidia-gb10-dspark-');
Object.assign(form, {
id: item.id,
containerName: item.containerName,
@@ -44,6 +44,12 @@ interface VllmAPI {
const vllmModules = import.meta.glob<VllmAPI>('@/xpack/api/modules/vllm.ts');
export const vllmAvailable = Object.keys(vllmModules).length > 0;
export function formatVllmVersion(version: string) {
return String(version || '')
.trim()
.replace(/^(nvidia|intel|ascend)-/i, '');
}
export async function loadVllmAPI() {
return Object.values(vllmModules)[0]();
}
+30 -4
View File
@@ -131,13 +131,25 @@
>
<span class="cell-ellipsis" :title="model.id">{{ model.id }}</span>
<el-button
v-if="index === 1 && row.models.length > 2"
v-if="!row.appInstallId && index === 1 && row.models.length > 2"
size="small"
class="model-more"
@click="poolRef.open(row)"
>
+{{ row.models.length - 2 }}
</el-button>
<el-tooltip
v-if="row.appInstallId && index === 1 && row.models.length > 2"
:content="
row.models
.slice(2)
.map((item) => item.id)
.join(', ')
"
placement="top"
>
<el-tag size="small" class="model-more">+{{ row.models.length - 2 }}</el-tag>
</el-tooltip>
</div>
<span v-if="!row.models?.length">—</span>
</template>
@@ -169,6 +181,7 @@
<fu-table-operations
:buttons="accountButtons"
:label="$t('commons.table.operate')"
align="left"
fixed="right"
:ellipsis="3"
width="220"
@@ -190,7 +203,12 @@
<el-table-column :label="$t('app.version')" min-width="150">
<template #default="{ row }">
<div class="flex items-center gap-2">
<span>{{ row.version || '—' }}</span>
<span>
{{
(row.engine === 'vllm' ? formatVllmVersion(row.version) : row.version) ||
'—'
}}
</span>
<el-button
v-if="row.upgradable && hasPermission('app_manage')"
v-permission
@@ -344,7 +362,14 @@ import ModelPool from './components/model-pool/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import SyncService from './components/sync/index.vue';
import ProviderLabel from '@/components/agent-provider-label/index.vue';
import { loadModelOverview, loadVllmAPI, readPages, vllmAvailable, type Deployment } from './deployment';
import {
formatVllmVersion,
loadModelOverview,
loadVllmAPI,
readPages,
vllmAvailable,
type Deployment,
} from './deployment';
const route = useRoute();
const router = useRouter();
@@ -547,6 +572,7 @@ const accountButtons = [
},
{
label: i18n.global.t('aiTools.agents.modelPool'),
disabled: (row: AI.AgentAccountItem) => row.provider === 'vllm',
click: (row: AI.AgentAccountItem) => poolRef.value.open(row),
},
{
@@ -592,7 +618,7 @@ const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart')
const taskID = newUUID();
await (await loadVllmAPI()).operateVllm({ id: row.id, operate: operation, taskID });
if (node !== currentNode.value) return;
if (row.version.toLowerCase().startsWith('nvidia-gb10-dspark-')) {
if (row.version.trim().toLowerCase().startsWith('nvidia-gb10-dspark-')) {
await search();
taskRef.value.openWithTaskID(taskID);
return;
@@ -8,7 +8,7 @@
<AppInstallForm ref="formRef" v-model="form" :loading="loading" sectioned hide-gpu-config>
<template #after-params>
<GPUConfig ref="gpuRef" v-model="form.gpu" />
<details open class="form-section">
<details ref="syncSectionRef" open class="form-section">
<summary class="form-section-header">
<span>{{ $t('aiTools.model.syncModelAccount') }}</span>
<el-switch
@@ -16,6 +16,7 @@
@keydown.stop
v-model="form.syncModelAccount"
:aria-label="$t('aiTools.model.syncModelAccount')"
@change="(enabled) => enabled && syncSectionRef && (syncSectionRef.open = true)"
/>
</summary>
<div v-show="form.syncModelAccount" class="form-section-body">
@@ -64,6 +65,7 @@ const open = ref(false);
const loading = ref(false);
const formRef = ref<InstanceType<typeof AppInstallForm>>();
const accountRef = ref();
const syncSectionRef = ref<HTMLDetailsElement>();
const gpuRef = ref<InstanceType<typeof GPUConfig>>();
const form = ref({
gpu: {
@@ -121,7 +121,7 @@
</div>
</component>
<GPUConfig v-if="showModelGPU" ref="gpuRef" v-model="paramModel.gpu" />
<details v-if="appKey === 'ollama'" open class="form-section">
<details v-if="appKey === 'ollama'" ref="syncSectionRef" open class="form-section">
<summary class="form-section-header">
<span>{{ $t('aiTools.model.syncModelAccount') }}</span>
<el-switch
@@ -129,6 +129,7 @@
@keydown.stop
v-model="paramModel.syncModelAccount"
:aria-label="$t('aiTools.model.syncModelAccount')"
@change="(enabled) => enabled && syncSectionRef && (syncSectionRef.open = true)"
/>
</summary>
<div v-show="paramModel.syncModelAccount" class="form-section-body">
@@ -298,6 +299,7 @@ const appType = ref('');
const appKey = ref('');
const showModelGPU = computed(() => props.modelGPU && appKey.value === 'ollama');
const accountRef = ref<InstanceType<typeof AccountAddress>>();
const syncSectionRef = ref<HTMLDetailsElement>();
const gpuRef = ref<InstanceType<typeof GPUConfig>>();
const appConfigUpdate = ref<App.AppConfigUpdate>({
installID: 0,