@@ -119,7 +119,7 @@ function _selectedServeTarget(panel) {
119119 port : host ? ( server ?. port || _getPort ( host ) || '' ) : '' ,
120120 env : server ?. env || '' ,
121121 venv,
122- platform : server ?. platform || _envState . platform || '' ,
122+ platform : host ? ( server ?. platform || '' ) : ( _envState . hostPlatform || '' ) ,
123123 label,
124124 } ;
125125}
@@ -240,6 +240,12 @@ function _runnableGgufFiles(model) {
240240 return primary . length ? primary : files ;
241241}
242242
243+ function _projectorGgufFiles ( model ) {
244+ return _ggufFilesForModel ( model )
245+ . filter ( f => ( f . role || '' ) === 'projector' || / ( ^ | \/ ) m m p r o j [ ^ / ] * \. g g u f $ / i. test ( f . rel_path || f . name || '' ) )
246+ . sort ( ( a , b ) => String ( a . rel_path || a . name || '' ) . localeCompare ( String ( b . rel_path || b . name || '' ) ) ) ;
247+ }
248+
243249function _ggufFileLabel ( file ) {
244250 const base = ( file . name || file . rel_path || '' ) . split ( '/' ) . pop ( ) ;
245251 const size = _formatGgufSize ( file . size_bytes ) ;
@@ -621,6 +627,7 @@ function _rerenderCachedModels() {
621627 : `This model's download isn't complete yet (${ esc ( m . size || 'partial' ) } ). The serve will start but is likely to crash on a missing shard. Wait for the download to finish, or relaunch after it's done.` ;
622628 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>` ;
623629 }
630+ 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>` ;
624631 // Row 1: Backend + Server + Env
625632 panelHtml += `<div class="hwfit-serve-row">` ;
626633 const backendOpts = _backendChoices . map ( ( [ v , l ] ) => `<option value="${ v } "${ defaultBackend === v ?' selected' :'' } >${ l } </option>` ) . join ( '' ) ;
@@ -896,18 +903,22 @@ function _rerenderCachedModels() {
896903 : m . is_local_dir && m . path
897904 ? `$({ find ${ _ldir } -name '*-00001-of-*.gguf' 2>/dev/null | sort; find ${ _ldir } -name '*.gguf' 2>/dev/null | sort; } | head -1)`
898905 : `$({ find ${ dir } -name '*-00001-of-*.gguf' 2>/dev/null | sort; find ${ dir } -name '*.gguf' 2>/dev/null | sort; } | head -1)` ;
899- // Vision: auto-find the mmproj (CLIP/projector ) file in the same dir .
900- // Resolved at runtime so the toggle just works if an mmproj-*.gguf is
901- // present (downloaded alongside the model). Empty if none → cmd omits it .
902- const _vsearchdir = ( m . is_local_dir && m . path ) ? _ldir : dir ;
903- f . _mmproj_path = `$(find ${ _vsearchdir } -iname 'mmproj*.gguf' 2>/dev/null | sort | head -1)` ;
906+ // Vision: use the scanned projector (CLIP/mmproj ) file when present .
907+ // Keeping this as a printf path avoids generating a command substitution
908+ // that the backend serve-command validator must reject as unsafe .
909+ const selectedProjector = _projectorGgufFiles ( m ) [ 0 ] ;
910+ f . _mmproj_path = selectedProjector ? _selectedGgufExpr ( m , repo , selectedProjector . rel_path ) : '' ;
904911 }
905912 if ( f . reasoning_parser ) {
906913 const _rpEl2 = panel . querySelector ( '[data-field="reasoning_parser"]' ) ;
907914 f . _reasoning_parser_value = _rpEl2 ?. dataset ?. parser || 'qwen3' ;
908915 }
909916 let cmd = _buildServeCmd ( f , serveModel , backend ) ;
910917 if ( f . extra && f . extra . trim ( ) ) cmd += ' ' + f . extra . trim ( ) ;
918+ const missingVisionProjector = backend === 'llamacpp' && ! ! f . vision && ! f . _mmproj_path ;
919+ panel . _visionMissingProjector = missingVisionProjector ;
920+ const _visionWarn = panel . querySelector ( '.hwfit-serve-vision-warn' ) ;
921+ if ( _visionWarn ) _visionWarn . style . display = missingVisionProjector ? 'flex' : 'none' ;
911922 const _ce2 = panel . querySelector ( '.hwfit-serve-cmd' ) ; _ce2 . value = cmd ; _ce2 . style . height = 'auto' ; _ce2 . style . height = _ce2 . scrollHeight + 'px' ;
912923 panel . _cmd = cmd ;
913924 panel . _host = f . host || '' ;
@@ -1982,12 +1993,16 @@ function _rerenderCachedModels() {
19821993 } ) ;
19831994 serveState . backend = serveState . backend || ( _detectBackend ( m ) . backend ) || 'vllm' ;
19841995 const launchTarget = _selectedServeTarget ( panel ) ;
1996+ 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 ) ) {
1997+ _restoreLaunchBtn ( ) ;
1998+ 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 ) ;
1999+ return ;
2000+ }
19852001 if ( serveState . backend === 'diffusers' && _remoteWindowsDiffusersUnsupported ( launchTarget ) ) {
19862002 _restoreLaunchBtn ( ) ;
19872003 uiModule . showToast ( 'Diffusers serving is not supported on remote Windows servers yet. Use local Windows or a Linux server.' , 9000 ) ;
19882004 return ;
19892005 }
1990-
19912006 // Pre-launch: check our own task list for a serve already running
19922007 // on this host. Offer to stop+launch as the default action — the
19932008 // SSH-based port probe below is more thorough but it can miss
0 commit comments