@@ -527,7 +527,7 @@ function _selectedServeTarget(panel) {
527527 env : server ?. env || '' ,
528528 port : host ? ( server ?. port || _getPort ( host ) || '' ) : '' ,
529529 venv,
530- platform : server ?. platform || _envState . platform || '' ,
530+ platform : host ? ( server ?. platform || '' ) : ( _envState . hostPlatform || '' ) ,
531531 label,
532532 } ;
533533}
@@ -658,6 +658,12 @@ function _selectedGgufSizeGb(model, relPath) {
658658 return bytes / ( 1024 ** 3 ) ;
659659}
660660
661+ function _projectorGgufFiles ( model ) {
662+ return _ggufFilesForModel ( model )
663+ . filter ( f => ( f . role || '' ) === 'projector' || / ( ^ | \/ ) m m p r o j [ ^ / ] * \. g g u f $ / i. test ( f . rel_path || f . name || '' ) )
664+ . sort ( ( a , b ) => String ( a . rel_path || a . name || '' ) . localeCompare ( String ( b . rel_path || b . name || '' ) ) ) ;
665+ }
666+
661667function _ggufFileLabel ( file ) {
662668 const base = ( file . name || file . rel_path || '' ) . split ( '/' ) . pop ( ) ;
663669 const size = _formatGgufSize ( file . size_bytes ) ;
@@ -1198,6 +1204,7 @@ function _rerenderCachedModels() {
11981204 panelHtml += `<div class="hwfit-serve-warn" style="margin:0 0 8px;padding:6px 10px;border-radius:5px;font-size:11px;background:color-mix(in srgb, var(--color-warning, #f0ad4e) 14%, transparent);border:1px solid color-mix(in srgb, var(--color-warning, #f0ad4e) 40%, transparent);color:var(--color-warning, #f0ad4e);display:flex;gap:6px;align-items:flex-start;line-height:1.4;"><span aria-hidden="true">⚠</span><span>${ _warnText } </span></div>` ;
11991205 }
12001206 panelHtml += `<div class="hwfit-serve-preset-row">${ _slotsHtml } </div>` ;
1207+ panelHtml += `<div class="hwfit-serve-vision-warn" style="display:none;margin:0 0 8px;padding:6px 10px;border-radius:5px;font-size:11px;background:color-mix(in srgb, var(--color-warning, #f0ad4e) 14%, transparent);border:1px solid color-mix(in srgb, var(--color-warning, #f0ad4e) 40%, transparent);color:var(--color-warning, #f0ad4e);gap:6px;align-items:flex-start;line-height:1.4;"><span aria-hidden="true">⚠</span><span>Vision is enabled, but no mmproj GGUF projector was found in the cached model scan. Download an mmproj-*.gguf for this model, then refresh the cached model list before launching.</span></div>` ;
12011208 // Row 1: Engine + Server + Env
12021209 panelHtml += `<div class="hwfit-serve-row">` ;
12031210 const backendOpts = _backendChoices . map ( ( [ v , l ] ) => `<option value="${ v } "${ defaultBackend === v ?' selected' :'' } >${ l } </option>` ) . join ( '' ) ;
@@ -1543,11 +1550,11 @@ function _rerenderCachedModels() {
15431550 : m . is_local_dir && m . path
15441551 ? `$({ find ${ _ldir } -name '*-00001-of-*.gguf' 2>/dev/null | sort; find ${ _ldir } -name '*.gguf' 2>/dev/null | sort; } | head -1)`
15451552 : `$({ find ${ dir } -name '*-00001-of-*.gguf' 2>/dev/null | sort; find ${ dir } -name '*.gguf' 2>/dev/null | sort; } | head -1)` ;
1546- // Vision: auto-find the mmproj (CLIP/projector ) file in the same dir .
1547- // Resolved at runtime so the toggle just works if an mmproj-*.gguf is
1548- // present (downloaded alongside the model). Empty if none → cmd omits it .
1549- const _vsearchdir = ( m . is_local_dir && m . path ) ? _ldir : dir ;
1550- f . _mmproj_path = `$(find ${ _vsearchdir } -iname 'mmproj*.gguf' 2>/dev/null | sort | head -1)` ;
1553+ // Vision: use the scanned projector (CLIP/mmproj ) file when present .
1554+ // Keeping this as a printf path avoids generating a command substitution
1555+ // that the backend serve-command validator must reject as unsafe .
1556+ const selectedProjector = _projectorGgufFiles ( m ) [ 0 ] ;
1557+ f . _mmproj_path = selectedProjector ? _selectedGgufExpr ( m , repo , selectedProjector . rel_path ) : '' ;
15511558 }
15521559 if ( f . reasoning_parser ) {
15531560 const _rpEl2 = panel . querySelector ( '[data-field="reasoning_parser"]' ) ;
@@ -1563,6 +1570,10 @@ function _rerenderCachedModels() {
15631570 }
15641571 let cmd = _buildServeCmd ( f , serveModel , backend ) ;
15651572 if ( f . extra && f . extra . trim ( ) ) cmd += ' ' + f . extra . trim ( ) ;
1573+ const missingVisionProjector = backend === 'llamacpp' && ! ! f . vision && ! f . _mmproj_path ;
1574+ panel . _visionMissingProjector = missingVisionProjector ;
1575+ const _visionWarn = panel . querySelector ( '.hwfit-serve-vision-warn' ) ;
1576+ if ( _visionWarn ) _visionWarn . style . display = missingVisionProjector ? 'flex' : 'none' ;
15661577 const _ce2 = panel . querySelector ( '.hwfit-serve-cmd' ) ; _ce2 . value = _formatServeCmdPreview ( cmd ) ; _ce2 . style . height = 'auto' ; _ce2 . style . height = _ce2 . scrollHeight + 'px' ;
15671578 panel . _cmd = cmd ;
15681579 panel . _host = f . host || '' ;
@@ -2938,12 +2949,16 @@ function _rerenderCachedModels() {
29382949 } ) ;
29392950 serveState . backend = serveState . backend || ( _detectBackend ( m ) . backend ) || 'vllm' ;
29402951 const launchTarget = _selectedServeTarget ( panel ) ;
2952+ if ( serveState . backend === 'llamacpp' && serveState . vision && ! / (?: ^ | \s ) (?: - - m m p r o j | - - c l i p _ m o d e l _ p a t h ) \b / . test ( launchCmd ) ) {
2953+ _restoreLaunchBtn ( ) ;
2954+ uiModule . showToast ( 'Vision is checked, but no mmproj projector is in the launch command. Refresh cached models after downloading mmproj, or add --mmproj manually.' , 8000 ) ;
2955+ return ;
2956+ }
29412957 if ( serveState . backend === 'diffusers' && _remoteWindowsDiffusersUnsupported ( launchTarget ) ) {
29422958 _restoreLaunchBtn ( ) ;
29432959 uiModule . showToast ( 'Diffusers serving is not supported on remote Windows servers yet. Use local Windows or a Linux server.' , 9000 ) ;
29442960 return ;
29452961 }
2946-
29472962 // Pre-launch: check our own task list for a serve already running
29482963 // on this host. Offer to stop+launch as the default action — the
29492964 // SSH-based port probe below is more thorough but it can miss
0 commit comments