From dc5dbef3fcdccff6de28e92fb4f26b76b6feb4ad Mon Sep 17 00:00:00 2001 From: rickychen-infinirc Date: Mon, 2 Feb 2026 19:34:02 +0800 Subject: [PATCH 1/8] feat: add Docker Compose YAML editor and native process recovery --- backend/app/services/deployment_sync.py | 67 ++ backend/pyproject.toml | 5 + backend/requirements.txt | 1 + frontend/package-lock.json | 90 +- frontend/package.json | 4 +- frontend/src/components/logos/index.tsx | 15 + frontend/src/pages/Deployments.tsx | 1251 ++++++++++++++--------- frontend/src/utils/dockerCompose.ts | 336 ++++++ worker/agent.py | 12 + worker/native_ops/process_manager.py | 121 +++ 10 files changed, 1432 insertions(+), 470 deletions(-) create mode 100644 frontend/src/utils/dockerCompose.ts diff --git a/backend/app/services/deployment_sync.py b/backend/app/services/deployment_sync.py index 3e9e55d..bf38b3f 100644 --- a/backend/app/services/deployment_sync.py +++ b/backend/app/services/deployment_sync.py @@ -122,6 +122,69 @@ async def check_with_semaphore(deployment: Deployment): return stats + def _is_native_deployment(self, deployment: Deployment) -> bool: + """Check if this is a native Mac deployment (not Docker).""" + # Native deployments have container_id like "native-123" + if deployment.container_id and deployment.container_id.startswith("native-"): + return True + + # Mac-only backends are always native + native_only_backends = {"mlx", "llama_cpp"} + if deployment.backend in native_only_backends: + return True + + # For Ollama, check if worker is Mac + if deployment.backend == BackendType.OLLAMA.value: + if deployment.worker and deployment.worker.is_mac: + return True + + return False + + async def _check_native_deployment(self, deployment: Deployment) -> str: + """Check a native Mac deployment's API health. + + Native deployments run as processes, not Docker containers. + We can only check if the API endpoint is responding. + """ + try: + # For native deployments, if worker is offline, keep current status + # and let the health check loop retry later (worker may be reconnecting) + if deployment.worker.status != "online": + logger.info( + f"Native deployment {deployment.name}: worker offline, " + "keeping current status (may be reconnecting)" + ) + # Don't change status - worker might be in the process of reconnecting + return "skipped" + + # Check API health via worker + api_healthy = await self._check_api_health( + deployment.worker.address, + deployment.port, + deployment.backend, + None, # No container_name for native + ) + + if api_healthy: + if deployment.status != DeploymentStatus.RUNNING.value: + deployment.status = DeploymentStatus.RUNNING.value + deployment.status_message = "Model ready (native process verified)" + logger.info(f"Native deployment {deployment.name}: healthy") + return "running_verified" + else: + # Process might have died or not started yet + # Mark as STARTING instead of ERROR to allow retry + deployment.status = DeploymentStatus.STARTING.value + deployment.status_message = "Native process not responding. Waiting for recovery..." + logger.info(f"Native deployment {deployment.name}: API not responding, waiting...") + return "api_not_ready" + + except Exception as e: + logger.error(f"Error checking native deployment {deployment.name}: {e}") + deployment.status = DeploymentStatus.STARTING.value + deployment.status_message = f"Checking status: {e}" + return "api_not_ready" + async def _check_and_update_deployment(self, deployment: Deployment, db) -> str: """Check a single deployment and update its status. @@ -134,6 +197,10 @@ async def _check_and_update_deployment(self, deployment: Deployment, db) -> str: logger.warning(f"Deployment {deployment.id} has no worker, skipping") return "skipped" + # Check if this is a native deployment (Mac without Docker) + if self._is_native_deployment(deployment): + return await self._check_native_deployment(deployment) + if not deployment.container_id: # If deployment is still starting, skip it if deployment.status == DeploymentStatus.STARTING.value: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b11ee91..6f3156e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -19,6 +19,11 @@ dependencies = [ "httpx>=0.26.0", "docker>=7.0.0", "python-multipart>=0.0.6", + "python-jose[cryptography]>=3.3.0", + "email-validator>=2.0.0", + "psutil>=5.9.0", + "optuna>=3.5.0", + "openai>=1.0.0", ] [project.optional-dependencies] diff --git a/backend/requirements.txt b/backend/requirements.txt index 0729e40..001cf1e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,3 +11,4 @@ python-jose[cryptography]>=3.3.0 email-validator>=2.0.0 psutil>=5.9.0 optuna>=3.5.0 +openai>=1.0.0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c2b2f48..ad6c288 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@ant-design/icons": "^5.2.6", + "@monaco-editor/react": "^4.7.0", "antd": "^5.12.0", "axios": "^1.6.0", "dayjs": "^1.11.10", @@ -20,7 +21,8 @@ "react-syntax-highlighter": "^16.1.0", "recharts": "^3.6.0", "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "yaml": "^2.8.2" }, "devDependencies": { "@testing-library/jest-dom": "^6.1.0", @@ -1252,6 +1254,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2195,6 +2220,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3668,6 +3701,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5674,6 +5717,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6646,6 +6702,17 @@ "dev": true, "license": "MIT" }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -8510,6 +8577,12 @@ "dev": true, "license": "MIT" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -9502,6 +9575,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "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/frontend/package.json b/frontend/package.json index 964a24d..308a97b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@ant-design/icons": "^5.2.6", + "@monaco-editor/react": "^4.7.0", "antd": "^5.12.0", "axios": "^1.6.0", "dayjs": "^1.11.10", @@ -25,7 +26,8 @@ "react-syntax-highlighter": "^16.1.0", "recharts": "^3.6.0", "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "yaml": "^2.8.2" }, "devDependencies": { "@testing-library/jest-dom": "^6.1.0", diff --git a/frontend/src/components/logos/index.tsx b/frontend/src/components/logos/index.tsx index 029b8b7..bf955ba 100644 --- a/frontend/src/components/logos/index.tsx +++ b/frontend/src/components/logos/index.tsx @@ -132,6 +132,21 @@ interface IconProps { className?: string; } +export function AppleIcon({ size = 14, style, className }: IconProps) { + return ( + + + + ); +} + export function DockerIcon({ size = 14, style, className }: IconProps) { return ( ( null, ); + // YAML editor state + const [showYamlPanel, setShowYamlPanel] = useState(true); // Show YAML panel on desktop + const [yamlContent, setYamlContent] = useState(""); + const [yamlError, setYamlError] = useState(null); + const [isYamlUserEditing, setIsYamlUserEditing] = useState(false); // Track if user is editing YAML + const yamlSyncTimeoutRef = useRef | null>(null); const { isMobile } = useResponsive(); const { isDark } = useAppTheme(); const { canEdit } = useAuth(); @@ -151,6 +166,141 @@ export default function Deployments() { const BACKEND_CONFIG = getBackendConfig(isDark); + // Check if backend is Docker-based (not native Mac) + const isDockerBackend = (backend: string, worker?: Worker | null) => { + // Native Mac backends + if (backend === "mlx" || backend === "llama_cpp") return false; + // Ollama on Mac is native + if (backend === "ollama" && worker?.os_type === "darwin") return false; + // vLLM on Mac (vLLM-Metal) is native + if (backend === "vllm" && worker?.os_type === "darwin") return false; + return true; + }; + + // Generate YAML from current form values + const generateYamlFromForm = useCallback(() => { + const values = form.getFieldsValue(); + // Use selectedModelId as fallback if form value not available yet + const modelId = values.model_id || selectedModelId; + const model = models.find((m) => m.id === modelId); + + if (!model) return ""; + + const config: DeploymentConfig = { + name: values.name || "deployment", + model_id: model.model_id, + model_name: model.name, + backend: selectedBackend, + worker_name: selectedWorker?.name, + gpu_indexes: + selectedGpuIndexes.length > 0 ? selectedGpuIndexes : undefined, + extra_params: values.extra_params || {}, + }; + + return generateDockerCompose(config); + }, [ + form, + models, + selectedModelId, + selectedBackend, + selectedWorker, + selectedGpuIndexes, + ]); + + // Watch form values for auto-sync to YAML + const formName = Form.useWatch("name", form); + const formModelId = Form.useWatch("model_id", form); + const formExtraParams = Form.useWatch("extra_params", form); + + // Auto-update YAML when form values change (if not user-editing YAML) + useEffect(() => { + if (!showYamlPanel) return; + if (isYamlUserEditing) return; // Don't overwrite user edits + if (!selectedModelId && !formModelId) return; + + const yaml = generateYamlFromForm(); + if (yaml && yaml !== yamlContent) { + setYamlContent(yaml); + } + }, [ + showYamlPanel, + isYamlUserEditing, + formName, + formModelId, + formExtraParams, + selectedModelId, + selectedBackend, + selectedWorker, + selectedGpuIndexes, + generateYamlFromForm, + yamlContent, + ]); + + // Handle YAML edit with debounced sync back to form + const handleYamlChange = useCallback( + (newYaml: string) => { + setYamlContent(newYaml); + setIsYamlUserEditing(true); + + // Validate YAML + const validation = validateDockerCompose(newYaml); + setYamlError(validation.valid ? null : validation.error || null); + + // Debounce sync to form + if (yamlSyncTimeoutRef.current) { + clearTimeout(yamlSyncTimeoutRef.current); + } + + if (validation.valid) { + yamlSyncTimeoutRef.current = setTimeout(() => { + const config = parseDockerCompose(newYaml); + if (config) { + // Update form fields that correspond to YAML values + if (config.name && config.name !== form.getFieldValue("name")) { + form.setFieldValue("name", config.name); + } + if (config.extra_params?.docker_image) { + form.setFieldValue( + ["extra_params", "docker_image"], + config.extra_params.docker_image, + ); + } + if (config.extra_params?.tensor_parallel_size !== undefined) { + form.setFieldValue( + ["extra_params", "tensor_parallel_size"], + config.extra_params.tensor_parallel_size, + ); + } + if (config.extra_params?.max_model_len !== undefined) { + form.setFieldValue( + ["extra_params", "max_model_len"], + config.extra_params.max_model_len, + ); + } + if (config.extra_params?.gpu_memory_utilization !== undefined) { + form.setFieldValue( + ["extra_params", "gpu_memory_utilization"], + config.extra_params.gpu_memory_utilization, + ); + } + } + // Reset editing flag after sync + setIsYamlUserEditing(false); + }, 500); // 500ms debounce + } + }, + [form], + ); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (yamlSyncTimeoutRef.current) { + clearTimeout(yamlSyncTimeoutRef.current); + } + }; + }, []); + const fetchDeployments = useCallback(async () => { try { const response = await deploymentsApi.list(); @@ -390,12 +540,22 @@ export default function Deployments() { display: "inline-flex", alignItems: "center", gap: 2, - background: "rgba(13, 148, 227, 0.1)", - border: "1px solid rgba(13, 148, 227, 0.3)", - color: "#0d94e3", + background: record.container_id.startsWith("native-") + ? "rgba(147, 147, 147, 0.1)" + : "rgba(13, 148, 227, 0.1)", + border: record.container_id.startsWith("native-") + ? "1px solid rgba(147, 147, 147, 0.3)" + : "1px solid rgba(13, 148, 227, 0.3)", + color: record.container_id.startsWith("native-") + ? "#666" + : "#0d94e3", }} > - + {record.container_id.startsWith("native-") ? ( + + ) : ( + + )} )} @@ -535,13 +695,28 @@ export default function Deployments() { display: "inline-flex", alignItems: "center", gap: 3, - background: "rgba(13, 148, 227, 0.1)", - border: "1px solid rgba(13, 148, 227, 0.3)", - color: "#0d94e3", + background: record.container_id.startsWith("native-") + ? "rgba(147, 147, 147, 0.1)" + : "rgba(13, 148, 227, 0.1)", + border: record.container_id.startsWith("native-") + ? "1px solid rgba(147, 147, 147, 0.3)" + : "1px solid rgba(13, 148, 227, 0.3)", + color: record.container_id.startsWith("native-") + ? "#666" + : "#0d94e3", }} > - - Docker + {record.container_id.startsWith("native-") ? ( + <> + + Native + + ) : ( + <> + + Docker + + )} )} @@ -769,7 +944,36 @@ export default function Deployments() { + New Deployment + {isDockerBackend(selectedBackend, selectedWorker) && !isMobile && ( + + )} + + } open={modalOpen} onCancel={() => { setModalOpen(false); @@ -777,509 +981,620 @@ export default function Deployments() { setSelectedWorkerId(null); setSelectedGpuIndexes([]); setSelectedBackend("vllm"); + setShowYamlPanel(true); + setYamlContent(""); + setYamlError(null); form.resetFields(); }} footer={null} - width={isMobile ? "100%" : 600} + width={ + isMobile + ? "100%" + : showYamlPanel && isDockerBackend(selectedBackend, selectedWorker) + ? 1100 + : 600 + } style={ isMobile ? { top: 20, maxWidth: "100%", margin: "0 8px" } : undefined } >
- - - - - - + + + + { - setSelectedWorkerId(value); - // Reset GPU selection when worker changes - setSelectedGpuIndexes([]); - form.setFieldValue("gpu_indexes", undefined); - // Check if current backend is available on the new worker - const newWorker = workers.find((w) => w.id === value); - const isMac = newWorker?.os_type === "darwin"; - const macBackends = ["ollama", "mlx", "llama_cpp", "vllm"]; - const linuxBackends = ["vllm", "sglang", "ollama"]; - const newAvailable = isMac ? macBackends : linuxBackends; - // Reset to first available backend if current is not available - if (!newAvailable.includes(selectedBackend)) { - const defaultBackend = isMac ? "vllm" : "vllm"; - setSelectedBackend( - defaultBackend as - | "vllm" - | "sglang" - | "ollama" - | "mlx" - | "llama_cpp", - ); - form.setFieldValue("backend", defaultBackend); - } - }} - options={workers.map((w) => ({ - label: ( - - - {w.name} ({w.address}) - - - {w.os_type === "darwin" && ( - macOS - )} - {w.gpu_info && w.gpu_info.length > 0 && ( - - {w.gpu_info.length} GPU - {w.gpu_info.length > 1 ? "s" : ""} - - )} - - - ), - value: w.id, - }))} - /> - + ), + value: m.id, + }; + })} + /> + - - { + setSelectedWorkerId(value); + // Reset GPU selection when worker changes + setSelectedGpuIndexes([]); + form.setFieldValue("gpu_indexes", undefined); + // Check if current backend is available on the new worker + const newWorker = workers.find((w) => w.id === value); + const isMac = newWorker?.os_type === "darwin"; + const macBackends = ["ollama", "mlx", "llama_cpp", "vllm"]; + const linuxBackends = ["vllm", "sglang", "ollama"]; + const newAvailable = isMac ? macBackends : linuxBackends; + // Reset to first available backend if current is not available + if (!newAvailable.includes(selectedBackend)) { + const defaultBackend = isMac ? "vllm" : "vllm"; + setSelectedBackend( + defaultBackend as + | "vllm" + | "sglang" + | "ollama" + | "mlx" + | "llama_cpp", + ); + form.setFieldValue("backend", defaultBackend); + } + }} + options={workers.map((w) => ({ + label: ( - {config.icon} + + {w.name} ({w.address}) + + + {w.os_type === "darwin" && ( + macOS + )} + {w.gpu_info && w.gpu_info.length > 0 && ( + + {w.gpu_info.length} GPU + {w.gpu_info.length > 1 ? "s" : ""} + + )} + - {label} - - ), - value: b, - }; - })} - /> - - - {/* macOS Ollama Warning - only show when Ollama backend is selected */} - {selectedWorker && - selectedWorker.os_type === "darwin" && - selectedBackend === "ollama" && - !selectedWorker.capabilities?.ollama && ( - -

- This Mac worker does not have Ollama installed. Please - install it first: -

-
-                      brew install ollama{"\n"}
-                      brew services start ollama
-                    
-

- After installation, the worker will detect Ollama on the - next heartbeat. -

- - } - type="error" - showIcon - style={{ marginBottom: 16 }} - /> - )} - - {/* macOS Ollama Not Running Warning - only show when Ollama backend is selected */} - {selectedWorker && - selectedWorker.os_type === "darwin" && - selectedBackend === "ollama" && - selectedWorker.capabilities?.ollama && - !selectedWorker.capabilities?.ollama_running && ( - -

- Ollama is installed but not running. Please start it: -

-
-                      brew services start ollama
-                    
- - } - type="warning" - showIcon - style={{ marginBottom: 16 }} - /> - )} - - {/* macOS Backend Info - show auto-install message */} - {selectedWorker && - selectedWorker.os_type === "darwin" && - selectedBackend === "vllm" && ( - - Uses Apple Silicon GPU acceleration. Will be automatically - installed on first deployment. - - } - type="info" - showIcon - style={{ marginBottom: 16 }} - /> - )} - {selectedWorker && - selectedWorker.os_type === "darwin" && - selectedBackend === "mlx" && ( - - Native Apple Silicon ML framework. Will be automatically - installed on first deployment. - - } - type="info" - showIcon - style={{ marginBottom: 16 }} - /> - )} - {selectedWorker && - selectedWorker.os_type === "darwin" && - selectedBackend === "llama_cpp" && ( - - High-performance inference with Metal acceleration. Will be - automatically installed via Homebrew on first deployment. - - } - type="info" - showIcon - style={{ marginBottom: 16 }} - /> - )} + ), + value: w.id, + }))} + /> + - {/* macOS Info */} - {selectedWorker && - selectedWorker.os_type === "darwin" && - selectedWorker.capabilities?.ollama_running && ( - -

- This worker supports native Apple Silicon backends: -

-
    -
  • - Ollama - Easiest, pull and run models - directly -
  • -
  • - MLX - Apple's ML framework, optimized - for Apple Silicon -
  • -
  • - llama.cpp - Cross-platform with Metal - acceleration -
  • -
-

- For MLX/llama.cpp, HuggingFace models will be - automatically converted if needed. -

- + - )} - - 0 - ? "Leave empty to use GPU 0" - : "No GPUs detected on this worker" - : "Select a worker first" - } - > - setSelectedBackend(value)} + options={availableBackends.map((b) => { + const config = BACKEND_CONFIG[b]; + // Show "vLLM-Metal" for vllm on Mac workers + const label = + b === "vllm" && selectedWorker?.os_type === "darwin" + ? "vLLM-Metal" + : config.label; + return { label: ( - - GPU {gpu.index}: {gpu.name} - - 0.5 - ? "green" - : "orange" - } - style={{ marginLeft: 8, fontSize: 11 }} + - {Math.round(gpu.memory_free / 1024 / 1024 / 1024)}GB - free - + {config.icon} + + {label} ), - value: gpu.index, - })) - : [{ label: GPU 0, value: 0 }] - } - /> - + value: b, + }; + })} + /> + - {/* Model Compatibility Check - Show when model is selected for vLLM/SGLang */} - {selectedModel && - selectedModel.source !== "ollama" && - !["mlx", "llama_cpp"].includes(selectedBackend) && ( - - )} + {/* macOS Ollama Warning - only show when Ollama backend is selected */} + {selectedWorker && + selectedWorker.os_type === "darwin" && + selectedBackend === "ollama" && + !selectedWorker.capabilities?.ollama && ( + +

+ This Mac worker does not have Ollama installed. Please + install it first: +

+
+                          brew install ollama{"\n"}
+                          brew services start ollama
+                        
+

+ After installation, the worker will detect Ollama on + the next heartbeat. +

+ + } + type="error" + showIcon + style={{ marginBottom: 16 }} + /> + )} - {/* Model Format Compatibility - Show for MLX/llama.cpp backends */} - {selectedModel && - selectedModel.source !== "ollama" && - ["mlx", "llama_cpp"].includes(selectedBackend) && ( - - )} + {/* macOS Ollama Not Running Warning - only show when Ollama backend is selected */} + {selectedWorker && + selectedWorker.os_type === "darwin" && + selectedBackend === "ollama" && + selectedWorker.capabilities?.ollama && + !selectedWorker.capabilities?.ollama_running && ( + +

+ Ollama is installed but not running. Please start it: +

+
+                          brew services start ollama
+                        
+ + } + type="warning" + showIcon + style={{ marginBottom: 16 }} + /> + )} + + {/* macOS Backend Info - show auto-install message */} + {selectedWorker && + selectedWorker.os_type === "darwin" && + selectedBackend === "vllm" && ( + + Uses Apple Silicon GPU acceleration. Will be + automatically installed on first deployment. + + } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> + )} + {selectedWorker && + selectedWorker.os_type === "darwin" && + selectedBackend === "mlx" && ( + + Native Apple Silicon ML framework. Will be automatically + installed on first deployment. + + } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> + )} + {selectedWorker && + selectedWorker.os_type === "darwin" && + selectedBackend === "llama_cpp" && ( + + High-performance inference with Metal acceleration. Will + be automatically installed via Homebrew on first + deployment. + + } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> + )} + + {/* macOS Info */} + {selectedWorker && + selectedWorker.os_type === "darwin" && + selectedWorker.capabilities?.ollama_running && ( + +

+ This worker supports native Apple Silicon backends: +

+
    +
  • + Ollama - Easiest, pull and run + models directly +
  • +
  • + MLX - Apple's ML framework, + optimized for Apple Silicon +
  • +
  • + llama.cpp - Cross-platform with + Metal acceleration +
  • +
+

+ For MLX/llama.cpp, HuggingFace models will be + automatically converted if needed. +

+ + } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> + )} - {/* Version Override - Show when model is selected (not for MLX/llama.cpp) */} - {selectedModelId && - !["mlx", "llama_cpp"].includes(selectedBackend) && ( 0 + ? "Leave empty to use GPU 0" + : "No GPUs detected on this worker" + : "Select a worker first" + } > ; + } + > + )[selectedBackend]?.versions || [] + ).map((v) => ({ + label: ( + + {v.version} + {v.recommended && ( + + Recommended + + )} + + ), + value: v.image, + }))} + /> + + )} + + {/* Advanced Parameters - Show when model is selected (not for MLX/llama.cpp) */} + {selectedModelId && + !["mlx", "llama_cpp"].includes(selectedBackend) && ( + + )} + + + {/* Right side: YAML Editor (desktop only, Docker backends only) */} + {showYamlPanel && + !isMobile && + isDockerBackend(selectedBackend, selectedWorker) && ( +
+
+ Docker Compose + {yamlError && ( + + {yamlError} + + )} +
+
+ handleYamlChange(value || "")} + onMount={() => setIsYamlUserEditing(false)} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: "on", + scrollBeyondLastLine: false, + wordWrap: "on", + tabSize: 2, + automaticLayout: true, + padding: { top: 8, bottom: 8 }, + }} + /> +
+
+ Form ↔ YAML auto-sync. Edit either side. +
+
+ )} + - +