// Container Image Selector - Main Logic
const CONFIG = {
versionsUrl: './versions/index.json',
versionsBaseUrl: './versions/',
registries: {
'docker-hub': {
name: 'Docker Hub', prefix: 'gpustack/', registry: 'docker.io',
// Special handling for Docker Hub: point to official image paths
overrides: { 'postgres': 'postgres', 'prometheus': 'prom/prometheus', 'grafana': 'grafana/grafana' }
},
'quay': { name: 'Quay.io', prefix: 'quay.io/gpustack/', registry: 'quay.io' },
'china': { name: '国内镜像源', prefix: 'swr.cn-south-1.myhuaweicloud.com/gpustack/', registry: 'swr.cn-south-1.myhuaweicloud.com' }
},
// Mapping of card types to acceleration frameworks
cardFrameworkMap: {
'nvidia': 'CUDA', 'amd': 'ROCm', 'ascend': 'CANN', 'hygon': 'DTK',
'mthreads': 'MUSA', 'iluvatar': 'CoreX', 'cambricon': 'Neuware',
'maca': 'MACA', 't-head': 'HGGC'
},
// Standard case mapping for inference engine names
backendNameMap: {
'vllm': 'vLLM', 'sglang': 'SGLang', 'mindie': 'MindIE', 'voxbox': 'VoxBox'
},
// Multilingual dictionary
i18n: {
'zh': {
'title': '容器镜像选择器',
'subtitle': '容器镜像选择器',
'back_to_docs': '文档',
'version': '版本',
'loading': '加载中...',
'select_config': '选择配置',
'gpu_type': 'GPU 类型',
'framework_version': '计算框架版本',
'select_gpu_first': '请选择 GPU 类型',
'inference_backend': '推理后端',
'inference_backend_tooltip': '如果未找到所需的内置推理后端或对应版本,可尝试切换到较低版本的计算框架。一般来说,高版本驱动能够兼容运行低版本的计算框架。',
'optional_images': '可选镜像',
'optional_postgres_images_tooltip': '使用外部数据库:https://docs.gpustack.ai/latest/installation/installation/#using-an-external-database',
'optional_monitoring_images_tooltip': '配置外部可观测性:https://docs.gpustack.ai/latest/user-guide/observability/#external-observability-optional',
'postgres': 'PostgreSQL',
'monitoring': '监控套件 (Prometheus + Grafana)',
'required_images': '所需镜像',
'architecture': '架构',
'registry': '镜像源',
'china_registry': '国内镜像源',
'tab_all': '全部',
'tab_server': 'Server 节点',
'tab_worker': 'Worker 节点',
'placeholder_select': '请在左侧选择配置以生成镜像列表',
'no_images': '# 未找到匹配的镜像',
'offline_guide_title': '需要离线安装?',
'guide_tab_save': '镜像文件导入 (Save & Load)',
'guide_tab_tag': '推送至私有仓库 (Tag & Push)',
'guide_tab_auto': '自动化镜像同步',
'guide_save_content': `
适用于完全物理隔离的环境,通过镜像文件离线导入。
- 在联网机器拉取镜像(参考上方镜像拉取命令)。
- 导出镜像为打包文件:
docker save -o gpustack-server-images.tar {{server_images}}\n\ndocker save -o gpustack-worker-images.tar {{worker_images}}
- 将镜像文件拷贝至离线机器。
- 按节点角色导入镜像:
Server 节点:
docker load -i gpustack-server-images.tar
Worker 节点:
docker load -i gpustack-worker-images.tar
Server + Worker 节点:
docker load -i gpustack-server-images.tar\ndocker load -i gpustack-worker-images.tar
-
运行 GPUStack Server 和 Worker 容器时通过
--system-default-container-registry 参数指定镜像源:
sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n {{registry}}/gpustack/gpustack:{{version}} \\\n --system-default-container-registry {{registry}}
`,
'guide_tag_content': `
适用于内网已有私有仓库(如 Harbor, Nexus)的场景。
- 在联网机器拉取镜像(参考上方镜像拉取命令)。
- 重新打标签并推送至私有仓库:
export PrivateRegistry=<您的私有仓库地址>\n{{tag_push_commands}}
- 运行 GPUStack Server 和 Worker 容器时,通过启动参数指定镜像源:
sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n $PrivateRegistry/gpustack/gpustack:{{version}} \\\n --system-default-container-registry $PrivateRegistry
`,
'guide_auto_content': `
若需要更自动化的镜像同步手段,GPUStack 提供镜像管理命令,用于同步与管理所需镜像:
gpustack copy-images:从源仓库同步镜像到目标仓库
gpustack save-images:下载并保存镜像到本地路径
gpustack load-images:导入本地镜像包
gpustack list-images:列出当前版本镜像清单
`,
'view_full_docs': '查看完整离线部署文档 →',
'copied': '已复制到剪贴板',
'no_images': '# 未找到匹配的镜像',
'comments': {
'main': 'GPUStack 镜像 - GPUStack 核心服务,Server 和 Worker 节点均需此镜像',
'runner': '推理后端镜像',
'pause': 'Pause 镜像 - 提供模型实例容器的共享网络和 IPC 环境,仅 Docker 环境需要',
'benchmark': 'Benchmark 镜像 - 用于运行模型性能基准测试',
'postgres': 'PostgreSQL - 用于独立部署外置数据库(可选组件)',
'monitoring': '监控套件 - 包含 Prometheus 和 Grafana(可选组件)'
},
'cards': {
'nvidia': 'NVIDIA', 'amd': 'AMD', 'ascend': '昇腾', 'hygon': '海光',
'mthreads': '摩尔线程', 'iluvatar': '天数智芯', 'cambricon': '寒武纪',
'maca': '沐曦', 't-head': '平头哥 PPU'
}
},
'en': {
'title': 'Container Image Selector',
'subtitle': 'Container Image Selector',
'back_to_docs': 'Docs',
'version': 'Version',
'loading': 'Loading...',
'select_config': 'Configuration',
'gpu_type': 'GPU Type',
'framework_version': 'Framework Version',
'select_gpu_first': 'Please select GPU Type first',
'inference_backend': 'Inference Backend',
'inference_backend_tooltip': 'If you cannot find the desired built-in inference backend or version, try switching the computing framework version to select a lower version image. High-version drivers are generally compatible with lower-version computing frameworks.',
'optional_images': 'Optional Images',
'optional_postgres_images_tooltip': 'Using an external database: https://docs.gpustack.ai/latest/installation/installation/#using-an-external-database',
'optional_monitoring_images_tooltip': 'Configuring external observability: https://docs.gpustack.ai/latest/user-guide/observability/#external-observability-optional',
'postgres': 'PostgreSQL',
'monitoring': 'Monitoring (Prometheus + Grafana)',
'required_images': 'Required Images',
'architecture': 'Architecture',
'registry': 'Registry',
'china_registry': 'China Mirror',
'tab_all': 'All',
'tab_server': 'Server Node',
'tab_worker': 'Worker Node',
'placeholder_select': 'Please select configuration on the left to generate image list',
'offline_guide_title': 'Need offline installation?',
'guide_tab_save': 'Image File Import (Save & Load)',
'guide_tab_tag': 'Push to Private Registry (Tag & Push)',
'guide_tab_auto': 'Automated Image Sync',
'guide_save_content': `
Suitable for completely air-gapped environments, importing images via files.
- Pull images on a machine with internet access (refer to commands above).
- Export images to tar files:
docker save -o gpustack-server-images.tar {{server_images}}\n\ndocker save -o gpustack-worker-images.tar {{worker_images}}
- Copy files to the offline machine.
- Import images by node role:
Server Node:
docker load -i gpustack-server-images.tar
Worker Node:
docker load -i gpustack-worker-images.tar
Server + Worker Node:
docker load -i gpustack-server-images.tar\ndocker load -i gpustack-worker-images.tar
-
When running GPUStack Server and Worker containers, specify the container registry using the
--system-default-container-registry parameter:
sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n {{registry}}/gpustack/gpustack:{{version}} \\\n --system-default-container-registry {{registry}}
`,
'guide_tag_content': `
Suitable for scenarios where a private registry (e.g., Harbor, Nexus) exists.
- Pull images on a machine with internet access (refer to commands above).
- Retag and push to the private registry:
export PrivateRegistry=<your-private-registry>\n{{tag_push_commands}}
- Specify the image registry via start parameters when running containers:
sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n $PrivateRegistry/gpustack/gpustack:{{version}} \\\n --system-default-container-registry $PrivateRegistry
`,
'guide_auto_content': `
For more automated sync methods, GPUStack provides image management commands:
gpustack copy-images: Sync images from source to destination registry
gpustack save-images: Download and save images to local path
gpustack load-images: Import images from local packages
gpustack list-images: List image manifest for current version
`,
'view_full_docs': 'View full air-gapped docs →',
'copied': 'Copied to clipboard',
'no_images': '# No matching images found',
'comments': {
'main': 'GPUStack Image - GPUStack core service, required for both Server and Worker nodes',
'runner': 'Inference Backend Images',
'pause': 'Pause Image - Provides shared network and IPC environment for model instance containers, required for Docker environment only',
'benchmark': 'Benchmark Image - Used for running model performance benchmarks',
'postgres': 'PostgreSQL - Used for independent deployment of external database (optional component)',
'monitoring': 'Monitoring Suite - Includes Prometheus and Grafana (optional components)'
},
'cards': {
'nvidia': 'NVIDIA', 'amd': 'AMD', 'ascend': 'Ascend', 'hygon': 'Hygon',
'mthreads': 'MThreads', 'iluvatar': 'Iluvatar', 'cambricon': 'Cambricon',
'maca': 'MetaX', 't-head': 'T-Head PPU'
}
}
}
};
// Global State - Default to English
let state = {
currentLang: localStorage.getItem('lang') || 'en',
images: [], runnerImages: [], supportMatrix: {},
selectedComponent: 'all', selectedArch: 'amd64', selectedRegistry: 'docker-hub',
selectedCard: null, selectedFrameworkVersion: null, selectedChipType: null,
selectedBackends: [], availableVersions: [], selectedGpuStackVersion: null,
optionalImages: { 'postgres': false, 'monitoring': false }
};
// DOM Elements
const elements = {};
// Initialization
async function init() {
initElements();
bindEvents();
updateLanguage();
await loadData();
// Show content after initialization
document.body.classList.add('i18n-ready');
}
// Initialize DOM elements
function initElements() {
elements.archSelector = document.getElementById('arch-selector');
elements.registrySelector = document.getElementById('registry-selector');
elements.gpustackVersionSelect = document.getElementById('gpustack-version-select');
elements.cardSelector = document.getElementById('card-selector');
elements.frameworkVersionSelect = document.getElementById('framework-version-select');
elements.backendSelector = document.getElementById('backend-selector');
elements.optionalImages = {
'postgres': document.getElementById('postgres'),
'monitoring': document.getElementById('monitoring')
};
elements.imageTabs = document.querySelectorAll('.image-tab');
elements.imageList = document.getElementById('image-list');
elements.copyAllBtn = document.getElementById('copy-all-btn');
elements.currentLangBtn = document.getElementById('current-lang');
elements.dropdownLinks = document.querySelectorAll('.dropdown-content a');
elements.registryChina = document.getElementById('registry-china');
elements.guideTabs = document.querySelectorAll('.guide-tab');
}
// Bind event listeners
function bindEvents() {
elements.archSelector.querySelectorAll('.option-button').forEach(btn => {
btn.addEventListener('click', () => selectArch(btn.dataset.value));
});
elements.registrySelector.querySelectorAll('.option-button').forEach(btn => {
btn.addEventListener('click', () => selectRegistry(btn.dataset.value));
});
elements.cardSelector.addEventListener('click', (e) => {
const btn = e.target.closest('.option-button');
if (btn) selectCard(btn.dataset.value);
});
elements.frameworkVersionSelect.addEventListener('change', (e) => selectFrameworkVersion(e.target.value));
elements.backendSelector.addEventListener('change', () => updateSelectedBackends());
Object.keys(elements.optionalImages).forEach(key => {
elements.optionalImages[key].addEventListener('change', () => {
state.optionalImages[key] = elements.optionalImages[key].checked;
generateImageList();
});
});
elements.gpustackVersionSelect.addEventListener('change', (e) => selectGpuStackVersion(e.target.value));
elements.imageTabs.forEach(tab => {
tab.addEventListener('click', () => selectComponent(tab.dataset.component));
});
elements.guideTabs.forEach(tab => {
tab.addEventListener('click', () => switchGuideTab(tab.dataset.guide));
});
elements.dropdownLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
setLanguage(link.dataset.lang);
});
});
}
// Set application language
function setLanguage(lang) {
if (state.currentLang === lang) return;
state.currentLang = lang;
localStorage.setItem('lang', lang);
updateLanguage();
// Auto-switch to Docker Hub if China Mirror is selected but language is English
if (state.currentLang === 'en' && state.selectedRegistry === 'china') {
selectRegistry('docker-hub');
}
renderCardSelector();
generateImageList();
}
// Get the current set of selected images
function getCurrentImages(component) {
const imgs = [];
if (state.selectedGpuStackVersion) imgs.push(getFullImageName('gpustack', state.selectedGpuStackVersion, state.selectedRegistry));
if (!state.selectedCard || !state.selectedFrameworkVersion) return imgs;
const fwTag = CONFIG.cardFrameworkMap[state.selectedCard].toLowerCase() + state.selectedFrameworkVersion;
if (component === 'worker' || component === 'all') {
state.runnerImages.forEach(img => {
const tag = img.replace('gpustack/runner:', '');
if (!tag.startsWith(fwTag)) return;
if (state.selectedCard === 'ascend' && state.selectedChipType) {
const pts = tag.split('-');
if (pts[1] && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b)) && pts[1] !== state.selectedChipType) return;
}
if (state.selectedBackends.length > 0) {
const pts = tag.split('-');
let bPart = (pts.length >= 3 && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b))) ? pts[2] : pts[1];
const m = bPart?.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
if (!m || !state.selectedBackends.includes(`${m[1].toLowerCase()}-${m[2]}`)) return;
}
imgs.push(getFullImageName('runner', tag, state.selectedRegistry));
});
const pause = state.images.find(i => i.includes('runtime:pause'));
if (pause) imgs.push(getFullImageName('runtime', pause.split(':')[1], state.selectedRegistry));
const bm = state.images.find(i => i.includes('benchmark-runner'));
if (bm) imgs.push(getFullImageName('benchmark-runner', bm.split(':')[1], state.selectedRegistry));
}
if (component === 'server' || component === 'all') {
if (state.optionalImages['postgres']) {
const pgs = state.images.filter(i => i.startsWith('postgres:'));
pgs.forEach(i => imgs.push(getFullImageName('postgres', i.split(':')[1], state.selectedRegistry)));
}
if (state.optionalImages['monitoring']) {
state.images.filter(i => i.includes('prometheus')).forEach(i => imgs.push(getFullImageName('prometheus', i.split(':')[1], state.selectedRegistry)));
state.images.filter(i => i.includes('grafana')).forEach(i => imgs.push(getFullImageName('grafana', i.split(':')[1], state.selectedRegistry)));
}
}
return imgs;
}
// Update UI text based on current language
function updateLanguage() {
const lang = state.currentLang;
const data = CONFIG.i18n[lang];
const version = state.selectedGpuStackVersion || 'latest';
// Translate page title
document.title = data.title;
elements.currentLangBtn.textContent = lang === 'zh' ? '简体中文' : 'English';
// Get current image lists for dynamic placeholders
const allImgs = getCurrentImages('all');
const serverImgs = getCurrentImages('server').join(' ');
const workerImgs = getCurrentImages('worker').join(' ');
// Generate tag & push commands under 'gpustack' namespace
const tagPushCmds = allImgs.map(img => {
const parts = img.split(':');
const namePart = parts[0];
const tagPart = parts[1];
const shortName = namePart.includes('/') ? namePart.split('/').pop() : namePart;
const destImg = `gpustack/${shortName}:${tagPart}`;
return `docker tag ${img} $PrivateRegistry/${destImg}\ndocker push $PrivateRegistry/${destImg}`;
}).join('\n');
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
if (data[key]) {
let content = data[key];
// Dynamically replace placeholders
content = content.replace(/{{version}}/g, version);
content = content.replace(/{{server_images}}/g, serverImgs || '<Server Image List>');
content = content.replace(/{{worker_images}}/g, workerImgs || '<Worker Image List>');
content = content.replace(/{{tag_push_commands}}/g, tagPushCmds || '# Please select configuration to generate commands');
content = content.replace(/{{registry}}/g, CONFIG.registries[state.selectedRegistry].registry);
el.innerHTML = content;
}
});
const copyImagesEl = document.getElementById('copy-images-command');
if (copyImagesEl) copyImagesEl.textContent = data.copy_images_cmd;
// Hide China Mirror in English mode
elements.registryChina.style.display = (lang === 'en') ? 'none' : 'block';
}
// Load versions and initial data
async function loadData() {
try {
const response = await fetch(CONFIG.versionsUrl);
const data = await response.json();
state.availableVersions = data.versions || data;
state.selectedGpuStackVersion = state.availableVersions[0];
renderVersionSelector();
await loadVersionData(state.selectedGpuStackVersion);
} catch (error) {
console.error('Failed to load data:', error);
showToast(state.currentLang === 'zh' ? '加载数据失败' : 'Failed to load data');
}
}
// Load specific version data
async function loadVersionData(version) {
const response = await fetch(`${CONFIG.versionsBaseUrl}${version}.json`);
const data = await response.json();
state.images = data;
state.runnerImages = data.filter(img => img.startsWith('gpustack/runner:'));
parseSupportMatrix();
renderCardSelector();
updateLanguage(); // Refresh offline guide version number
}
// Render version dropdown
function renderVersionSelector() {
elements.gpustackVersionSelect.innerHTML = '';
state.availableVersions.forEach(v => {
const opt = document.createElement('option');
opt.value = opt.textContent = v;
opt.selected = (v === state.selectedGpuStackVersion);
elements.gpustackVersionSelect.appendChild(opt);
});
}
// Handle version change
async function selectGpuStackVersion(v) {
state.selectedGpuStackVersion = v;
await loadVersionData(v);
generateImageList();
}
// Parse supported hardware and backends from images
function parseSupportMatrix() {
const matrix = { frameworks: {} };
state.runnerImages.forEach(image => {
const tag = image.replace('gpustack/runner:', '');
let parts = tag.split('-');
const fwMatch = parts[0].match(/^([a-z]+)([\d.]+)$/i);
if (!fwMatch) return;
const fw = fwMatch[1].toLowerCase();
if (!matrix.frameworks[fw]) matrix.frameworks[fw] = { versions: new Set(), cards: new Set(), backends: new Set() };
matrix.frameworks[fw].versions.add(fwMatch[2]);
let hasCard = false;
if (parts.length >= 2 && /^[a-z0-9]+$/i.test(parts[1]) && !/^\d/.test(parts[1]) && !['vllm','sglang','mindie','voxbox'].some(b => parts[1].includes(b))) {
matrix.frameworks[fw].cards.add(parts[1]);
hasCard = true;
}
const backendPart = hasCard ? parts[2] : parts[1];
if (backendPart) {
const bMatch = backendPart.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
if (bMatch) matrix.frameworks[fw].backends.add(bMatch[1].toLowerCase());
}
});
state.supportMatrix = { frameworks: {} };
Object.keys(matrix.frameworks).forEach(fw => {
state.supportMatrix.frameworks[fw] = {
versions: Array.from(matrix.frameworks[fw].versions),
cards: Array.from(matrix.frameworks[fw].cards),
backends: Array.from(matrix.frameworks[fw].backends)
};
});
}
// Render GPU Type buttons
function renderCardSelector() {
elements.cardSelector.innerHTML = '';
const cardDict = CONFIG.i18n[state.currentLang].cards;
const allCards = [
{ id: 'nvidia', name: cardDict.nvidia, framework: 'CUDA' },
{ id: 'amd', name: cardDict.amd, framework: 'ROCm' },
{ id: 'ascend', name: cardDict.ascend, framework: 'CANN' },
{ id: 'hygon', name: cardDict.hygon, framework: 'DTK' },
{ id: 'mthreads', name: cardDict.mthreads, framework: 'MUSA' },
{ id: 'iluvatar', name: cardDict.iluvatar, framework: 'CoreX' },
{ id: 'cambricon', name: cardDict.cambricon, framework: 'Neuware' },
{ id: 'maca', name: cardDict.maca, framework: 'MACA' },
{ id: 't-head', name: cardDict.t_head || cardDict['t-head'], framework: 'HGGC' }
];
allCards.forEach(card => {
const framework = card.framework.toLowerCase();
const hasData = state.supportMatrix.frameworks[framework];
const btn = document.createElement('button');
btn.className = 'option-button';
btn.dataset.value = card.id;
btn.innerHTML = `${card.name}`;
if (card.id === 'cambricon') {
const tip = state.currentLang === 'zh' ? '请联系寒武纪厂商获取推理后端镜像' : 'Please contact Cambricon vendor for inference backend images';
btn.innerHTML += `i ${tip}`;
}
btn.disabled = !hasData;
if (!hasData) btn.title = state.currentLang === 'zh' ? '暂无可用镜像' : 'No images available';
elements.cardSelector.appendChild(btn);
});
const activeBtn = elements.cardSelector.querySelector(`[data-value="${state.selectedCard}"]`) || elements.cardSelector.querySelector('[data-value="nvidia"]');
if (activeBtn && !activeBtn.disabled) selectCard(activeBtn.dataset.value);
}
// Handle GPU Type selection
function selectCard(cardId) {
state.selectedCard = cardId;
elements.cardSelector.querySelectorAll('.option-button').forEach(b => b.classList.toggle('active', b.dataset.value === cardId));
const fw = CONFIG.cardFrameworkMap[cardId].toLowerCase();
renderFrameworkVersions(fw, cardId);
const first = elements.frameworkVersionSelect.querySelector('option:not([value=""])');
if (first) { first.selected = true; selectFrameworkVersion(first.value); }
const targetArch = cardId === 'ascend' ? 'arm64' : 'amd64';
if (state.selectedArch !== targetArch) selectArch(targetArch);
}
// Render Framework Version dropdown
function renderFrameworkVersions(fw, cardId) {
const select = elements.frameworkVersionSelect;
const placeholder = state.currentLang === 'zh' ? '请选择版本' : 'Please select version';
select.innerHTML = ``;
const versions = state.supportMatrix.frameworks[fw]?.versions || [];
const fwName = CONFIG.cardFrameworkMap[cardId];
if (cardId === 'ascend') {
const combos = new Map();
state.runnerImages.forEach(img => {
const tag = img.replace('gpustack/runner:', '');
if (!tag.startsWith(fw) || !tag.startsWith('cann')) return;
const pts = tag.split('-');
if (pts.length < 2) return;
const vMatch = pts[0].match(/^cann([\d.]+)$/i);
if (vMatch && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b))) combos.set(`${vMatch[1]}-${pts[1]}`, { v: vMatch[1], c: pts[1] });
});
Array.from(combos.values()).sort((a,b) => {
const versionCompare = b.v.localeCompare(a.v, undefined, {numeric: true, sensitivity: 'base'});
if (versionCompare !== 0) {
return versionCompare;
}
const aIs910B = a.c.toUpperCase().includes('910B');
const bIs910B = b.c.toUpperCase().includes('910B');
if (aIs910B && !bIs910B) return -1;
if (!aIs910B && bIs910B) return 1;
return b.c.localeCompare(a.c);
}).forEach(item => {
const opt = document.createElement('option');
opt.value = item.v; opt.dataset.chipType = item.c; opt.textContent = `${fwName} ${item.v} (${item.c})`;
select.appendChild(opt);
});
} else {
versions.sort((a,b) => b.localeCompare(a)).forEach(v => {
const opt = document.createElement('option');
opt.value = v; opt.textContent = `${fwName} ${v}`;
select.appendChild(opt);
});
}
}
// Handle Framework Version selection
function selectFrameworkVersion(v) {
state.selectedFrameworkVersion = v;
const opt = elements.frameworkVersionSelect.selectedOptions[0];
state.selectedChipType = opt?.dataset.chipType || null;
const fw = CONFIG.cardFrameworkMap[state.selectedCard].toLowerCase();
renderBackends(fw, v, state.selectedChipType);
state.selectedBackends = [];
generateImageList();
}
// Render Inference Backend checkboxes
function renderBackends(fw, v, chip) {
elements.backendSelector.innerHTML = '';
if (!v) return;
const fwTag = fw + v;
const options = new Map();
state.runnerImages.forEach(img => {
const tag = img.replace('gpustack/runner:', '');
if (!tag.startsWith(fwTag)) return;
const pts = tag.split('-');
let bPart = pts[1];
let hasCard = pts.length >= 3 && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b));
if (hasCard) { if (state.selectedCard === 'ascend' && pts[1] !== chip) return; bPart = pts[2]; }
const m = bPart?.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
if (m) options.set(`${m[1]}-${m[2]}`, `${CONFIG.backendNameMap[m[1].toLowerCase()] || m[1]} ${m[2]}`);
});
// Sort versions in reverse order
Array.from(options.entries()).sort((a,b) => b[0].localeCompare(a[0])).forEach(([k, name]) => {
const lbl = document.createElement('label'); lbl.className = 'checkbox-item';
lbl.innerHTML = `${name}`;
elements.backendSelector.appendChild(lbl);
});
}
// Update selected backends
function updateSelectedBackends() {
state.selectedBackends = Array.from(elements.backendSelector.querySelectorAll('input:checked')).map(i => i.value);
generateImageList();
}
// Get full image name supporting Overrides and Registry logic
function getFullImageName(baseName, tag, registryKey) {
const reg = CONFIG.registries[registryKey];
let path = `${reg.prefix}${baseName}`;
if (reg.overrides && reg.overrides[baseName]) {
path = reg.overrides[baseName];
}
return `${path}:${tag}`;
}
// Main logic to generate the list of docker pull commands
function generateImageList() {
const plat = `--platform linux/${state.selectedArch}`;
const cmds = [];
const isServer = state.selectedComponent === 'server' || state.selectedComponent === 'all';
const isWorker = state.selectedComponent === 'worker' || state.selectedComponent === 'all';
const t = CONFIG.i18n[state.currentLang];
if (state.selectedGpuStackVersion) {
cmds.push(`# ${t.comments.main}`);
cmds.push(`docker pull ${plat} ${getFullImageName('gpustack', state.selectedGpuStackVersion, state.selectedRegistry)}`);
}
if (!state.selectedCard || !state.selectedFrameworkVersion) {
elements.imageList.textContent = cmds.length ? cmds.join('\n') : t.placeholder_select;
elements.copyAllBtn.style.display = cmds.length ? 'flex' : 'none';
if (cmds.length) elements.copyAllBtn.onclick = () => copyToClipboard(cmds.join('\n'));
updateLanguage();
return;
}
const fwTag = CONFIG.cardFrameworkMap[state.selectedCard].toLowerCase() + state.selectedFrameworkVersion;
if (isWorker) {
const rCmds = [];
state.runnerImages.forEach(img => {
const tag = img.replace('gpustack/runner:', '');
if (!tag.startsWith(fwTag)) return;
if (state.selectedCard === 'ascend' && state.selectedChipType) {
const pts = tag.split('-');
if (pts[1] && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b)) && pts[1] !== state.selectedChipType) return;
}
if (state.selectedBackends.length > 0) {
const pts = tag.split('-');
let bPart = (pts.length >= 3 && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b))) ? pts[2] : pts[1];
const m = bPart?.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
if (!m || !state.selectedBackends.includes(`${m[1].toLowerCase()}-${m[2]}`)) return;
}
rCmds.push(`docker pull ${plat} ${getFullImageName('runner', tag, state.selectedRegistry)}`);
});
if (rCmds.length) {
const comment = t.comments.runner;
cmds.push(`# ${comment}`);
cmds.push(...rCmds);
}
const pause = state.images.find(i => i.includes('runtime:pause'));
if (pause) {
const comment = t.comments.pause;
cmds.push(`# ${comment}`);
cmds.push(`docker pull ${plat} ${getFullImageName('runtime', pause.split(':')[1], state.selectedRegistry)}`);
}
const bm = state.images.find(i => i.includes('benchmark-runner'));
if (bm) {
const comment = t.comments.benchmark;
cmds.push(`# ${comment}`);
cmds.push(`docker pull ${plat} ${getFullImageName('benchmark-runner', bm.split(':')[1], state.selectedRegistry)}`);
}
}
if (isServer) {
if (state.optionalImages['postgres']) {
const pgs = state.images.filter(i => i.startsWith('postgres:'));
if (pgs.length) {
const comment = t.comments.postgres;
cmds.push(`# ${comment}`);
pgs.forEach(i => cmds.push(`docker pull ${plat} ${getFullImageName('postgres', i.split(':')[1], state.selectedRegistry)}`));
}
}
if (state.optionalImages['monitoring']) {
const comment = t.comments.monitoring;
cmds.push(`# ${comment}`);
state.images.filter(i => i.includes('prometheus')).forEach(i => cmds.push(`docker pull ${plat} ${getFullImageName('prometheus', i.split(':')[1], state.selectedRegistry)}`));
state.images.filter(i => i.includes('grafana')).forEach(i => cmds.push(`docker pull ${plat} ${getFullImageName('grafana', i.split(':')[1], state.selectedRegistry)}`));
}
}
renderImageList(cmds);
updateLanguage();
}
// Display the generated commands in the output area
function renderImageList(cmds) {
if (!cmds.length) { elements.imageList.textContent = CONFIG.i18n[state.currentLang].no_images; elements.copyAllBtn.style.display = 'none'; return; }
elements.imageList.textContent = cmds.join('\n');
elements.copyAllBtn.style.display = 'flex';
elements.copyAllBtn.onclick = () => copyToClipboard(cmds.join('\n'));
}
// Interaction handlers
function selectArch(v) { state.selectedArch = v; elements.archSelector.querySelectorAll('.option-button').forEach(b => b.classList.toggle('active', b.dataset.value === v)); generateImageList(); }
function selectRegistry(v) { state.selectedRegistry = v; elements.registrySelector.querySelectorAll('.option-button').forEach(b => b.classList.toggle('active', b.dataset.value === v)); generateImageList(); }
function selectComponent(v) { state.selectedComponent = v; elements.imageTabs.forEach(t => t.classList.toggle('active', t.dataset.component === v)); generateImageList(); }
function switchGuideTab(id) { elements.guideTabs.forEach(t => t.classList.toggle('active', t.dataset.guide === id)); document.querySelectorAll('.guide-content').forEach(c => c.classList.toggle('active', c.id === `guide-${id}`)); }
// Utility to copy text to clipboard
async function copyToClipboard(text) {
try { await navigator.clipboard.writeText(text); showToast(CONFIG.i18n[state.currentLang].copied); }
catch (err) { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); showToast(CONFIG.i18n[state.currentLang].copied); }
}
// Show temporary toast message
function showToast(msg) {
const t = document.createElement('div'); t.className = 'toast'; t.textContent = msg; document.body.appendChild(t);
setTimeout(() => t.remove(), 2000);
}
// Start app when DOM is ready
document.addEventListener('DOMContentLoaded', init);