Skip to content

Commit 486a6a7

Browse files
zoomdbzentitycs
authored andcommitted
fix(cookbook): treat local Windows as Windows for serve commands (odysseus-dev#3975)
* fix(cookbook): prefer native llama-server on local Windows * fix(cookbook): harden local llama-server launch commands * fix(cookbook): build serve commands for selected target
1 parent e1be533 commit 486a6a7

7 files changed

Lines changed: 292 additions & 216 deletions

File tree

routes/cookbook_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ def _bash_squote(v: str) -> str:
561561
# Allow-list of binaries permitted as the leading token of `req.cmd` for /api/model/serve.
562562
# Anything else is rejected before the cmd is interpolated into a tmux/PowerShell wrapper.
563563
_SERVE_CMD_ALLOWLIST = {
564-
"vllm", "llama-server", "llama_server", "llama.cpp", "ollama",
564+
"vllm", "llama-server", "llama-server.exe", "llama_server", "llama.cpp", "ollama",
565565
"python", "python3",
566566
"sglang", "lmdeploy",
567567
"node", "npx",

routes/cookbook_routes.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ def _mask_secret(value: str) -> str:
7474
return "stored"
7575
return f"{value[:4]}...{value[-4:]}"
7676

77+
def _client_host_platform() -> str:
78+
return "windows" if IS_WINDOWS else ""
79+
7780
def _decrypt_secret(value: str | None) -> str:
7881
if not value:
7982
return ""
@@ -246,11 +249,15 @@ def _state_for_client(state):
246249
"""Return cookbook state without raw secrets for browser clients."""
247250
_strip_task_secrets(state)
248251
env = state.get("env") if isinstance(state, dict) else None
252+
if isinstance(state, dict) and not isinstance(env, dict):
253+
env = {}
254+
state["env"] = env
249255
if isinstance(env, dict):
250256
token = _decrypt_secret(env.get("hfToken"))
251257
env.pop("hfToken", None)
252258
env["hfTokenConfigured"] = bool(token)
253259
env["hfTokenMasked"] = _mask_secret(token)
260+
env["hostPlatform"] = _client_host_platform()
254261
return state
255262

256263
def _state_for_storage(state, on_disk=None):
@@ -269,6 +276,7 @@ def _state_for_storage(state, on_disk=None):
269276
env.pop("hfToken", None)
270277
env.pop("hfTokenMasked", None)
271278
env.pop("hfTokenConfigured", None)
279+
env.pop("hostPlatform", None)
272280
return state
273281

274282
def _load_stored_hf_token() -> str:
@@ -1528,6 +1536,10 @@ async def model_serve(request: Request, req: ServeRequest):
15281536
# shell resolves the bundled python3/hf, mirroring the download flow.
15291537
if not remote:
15301538
runner_lines.append(_local_tooling_path_export(sys.executable))
1539+
if local_windows:
1540+
# Detached Git Bash runs do not always inherit recently edited
1541+
# user PATH entries from the already-running Odysseus process.
1542+
runner_lines.append('export PATH="$HOME/bin:$HOME/llama.cpp/build-cuda/bin/Release:$HOME/llama.cpp/build/bin/Release:$HOME/llama.cpp/build/bin/Debug:$HOME/llama.cpp/build/bin:$PATH"')
15311543
runner_lines.append("export FLASHINFER_DISABLE_VERSION_CHECK=1")
15321544
if req.hf_token:
15331545
runner_lines.append(f"export HF_TOKEN='{_bash_squote(req.hf_token)}'")
@@ -1542,7 +1554,8 @@ async def model_serve(request: Request, req: ServeRequest):
15421554
runner_lines.append(_HF_TOKEN_STATUS_SNIPPET)
15431555
handled_ollama_serve = False
15441556
# Auto-install inference engine if missing
1545-
if "llama_cpp" in req.cmd or "llama-server" in req.cmd:
1557+
local_windows_llama_cmd = local_windows and ("llama_cpp" in req.cmd or "llama-server" in req.cmd)
1558+
if ("llama_cpp" in req.cmd or "llama-server" in req.cmd) and not local_windows_llama_cmd:
15461559
# Prefer the NATIVE llama-server binary — its minja templating
15471560
# renders modern GGUF chat templates that the Python bindings'
15481561
# Jinja2 rejects (do_tojson ensure_ascii). Build it once from
@@ -2445,8 +2458,8 @@ async def get_cookbook_state(request: Request):
24452458
try:
24462459
return _state_for_client(json.loads(_cookbook_state_path.read_text(encoding="utf-8")))
24472460
except Exception:
2448-
return {}
2449-
return {}
2461+
return _state_for_client({})
2462+
return _state_for_client({})
24502463

24512464
@router.post("/api/cookbook/state")
24522465
async def save_cookbook_state(request: Request):

static/js/cookbook.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function _platformIcon(platform) {
7676
return '';
7777
}
7878

79-
export let _envState = { env: 'none', envPath: '', hfToken: '', hfTokenConfigured: false, hfTokenMasked: '', gpus: '', remoteHost: '', servers: [], modelPaths: [], platform: '', defaultServer: '' };
79+
export let _envState = { env: 'none', envPath: '', hfToken: '', hfTokenConfigured: false, hfTokenMasked: '', gpus: '', remoteHost: '', servers: [], modelPaths: [], platform: '', hostPlatform: '', defaultServer: '' };
8080
let _lastCacheHostVal = null;
8181
let _cookbookOpeningSpinners = [];
8282
export function _lastCacheHost() { return _lastCacheHostVal; }
@@ -213,8 +213,13 @@ function _getPort(hostOrTask) {
213213

214214
/** Get platform for a given host (or task object). Returns 'windows', 'termux', 'linux', or '' */
215215
export function _getPlatform(hostOrTask) {
216-
if (!hostOrTask) return _envState.platform || '';
217-
if (typeof hostOrTask === 'object') return hostOrTask.platform || _getPlatform(hostOrTask.remoteServerKey || hostOrTask.remoteHost);
216+
if (hostOrTask === 'local') return _envState.hostPlatform || '';
217+
if (!hostOrTask) return _envState.remoteHost ? (_envState.platform || '') : (_envState.hostPlatform || '');
218+
if (typeof hostOrTask === 'object') {
219+
const taskHost = hostOrTask.remoteServerKey || hostOrTask.remoteHost || '';
220+
if (!taskHost || taskHost === 'local') return _envState.hostPlatform || '';
221+
return hostOrTask.platform || _getPlatform(taskHost);
222+
}
218223
const selected = hostOrTask === _envState.remoteHost ? _selectedServer() : null;
219224
const srv = selected || _serverByVal(hostOrTask);
220225
return srv?.platform || '';
@@ -638,7 +643,12 @@ export function _buildServeCmd(f, modelName, backend) {
638643
// GPU list — read from gpus (button strip); fall back to gpu_id for
639644
// backward-compat with older saved presets that pre-date the removal.
640645
const gpuId = (f.gpus || f.gpu_id || '').toString().trim();
641-
const py = _isWindows() ? 'python' : 'python3';
646+
const _targetHost = Object.prototype.hasOwnProperty.call(f, 'host')
647+
? String(f.host || '').trim()
648+
: String(_envState.remoteHost || '').trim();
649+
const _isWin = _targetHost ? _isWindows(_targetHost) : _isWindows('local');
650+
const _localWindows = _isWin && !_targetHost;
651+
const py = _isWin ? 'python' : 'python3';
642652
// CPU-only serve (-ngl 0): drop the GPU-only flags, otherwise the command
643653
// mixes "zero GPU layers" with CUDA unified-memory + flash-attn and fails to
644654
// start (issue #1291). Only affects the ngl=0 path; GPU serving is unchanged.
@@ -660,19 +670,19 @@ export function _buildServeCmd(f, modelName, backend) {
660670
// with misleading prefixes.
661671
const _sb = String(_hwfitCache?.system?.backend || '').toLowerCase();
662672
const _hwfitHost = String(_hwfitCache?._scannedHost || '');
663-
const _curHost = String(_envState.remoteHost || '');
673+
const _curHost = _targetHost;
664674
const _isCudaTarget = (_sb === 'cuda') && (_hwfitHost === _curHost);
665675
const lcPrefix = (() => {
666676
let p = '';
667-
if (f.unified_mem && !_cpuOnly && !_isWindows() && _isCudaTarget) p += `GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 `;
668-
// No GPU env var in CPU mode `-ngl 0` already disables offload
677+
if (f.unified_mem && !_cpuOnly && (!_isWin || _localWindows) && _isCudaTarget) p += `GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 `;
678+
// No GPU env var in CPU mode - `-ngl 0` already disables offload
669679
// so CUDA_VISIBLE_DEVICES / HIP_VISIBLE_DEVICES would be misleading
670680
// clutter ("why is CUDA pinned for a CPU run?").
671-
if (!_isWindows() && !_cpuOnly) p += _gpuEnvPrefix(gpuId);
681+
if ((!_isWin || _localWindows) && !_cpuOnly) p += _gpuEnvPrefix(gpuId);
672682
return p;
673683
})();
674-
if (f.unified_mem && !_cpuOnly && _isWindows() && _isCudaTarget) cmd += `$env:GGML_CUDA_ENABLE_UNIFIED_MEMORY="1"; `;
675-
if (_isWindows() && !_cpuOnly) cmd += _gpuEnvPrefix(gpuId, true);
684+
if (f.unified_mem && !_cpuOnly && _isWin && !_localWindows && _isCudaTarget) cmd += `$env:GGML_CUDA_ENABLE_UNIFIED_MEMORY="1"; `;
685+
if (_isWin && !_localWindows && !_cpuOnly) cmd += _gpuEnvPrefix(gpuId, true);
676686
const needsGgufPrelude = /^\$\(\{\s*find\s/.test(String(ggufPath || ''));
677687
const modelArg = needsGgufPrelude ? '"$MODEL_FILE"' : `"${ggufPath}"`;
678688
// Prefer native llama-server. The backend bootstrap resolves/builds the
@@ -744,11 +754,16 @@ export function _buildServeCmd(f, modelName, backend) {
744754
// llama-cpp-python takes the projector via --clip_model_path.
745755
_lcpExtra += ` --clip_model_path "${f._mmproj_path}"`;
746756
}
747-
if (_isWindows()) {
748-
const _lcpServer = `${lcPrefix}${py} -m llama_cpp.server --model ${modelArg} --host 0.0.0.0 --port ${f.port || '8080'} --n_gpu_layers ${f.ngl || '99'} --n_ctx ${f.ctx || '8192'}${_lcpExtra}`;
757+
const _lcServer = `${lcPrefix}llama-server --model ${modelArg} --host 0.0.0.0 --port ${f.port || '8080'} -ngl ${f.ngl || '99'} -c ${f.ctx || '8192'}${_lcExtra}`;
758+
const _lcpServer = `${lcPrefix}${py} -m llama_cpp.server --model ${modelArg} --host 0.0.0.0 --port ${f.port || '8080'} --n_gpu_layers ${f.ngl || '99'} --n_ctx ${f.ctx || '8192'}${_lcpExtra}`;
759+
if (_localWindows) {
760+
// Local Windows serve is launched through Git Bash, so use the native
761+
// llama-server shape and let PATH resolve the CUDA Release wrapper.
762+
cmd += _lcServer;
763+
} else if (_isWin) {
749764
cmd += _lcpServer;
750765
} else {
751-
cmd += `${lcPrefix}llama-server --model ${modelArg} --host 0.0.0.0 --port ${f.port || '8080'} -ngl ${f.ngl || '99'} -c ${f.ctx || '8192'}${_lcExtra}`;
766+
cmd += _lcServer;
752767
}
753768
if (needsGgufPrelude) {
754769
cmd = `MODEL_FILE=${ggufPath} && { [ -n "$MODEL_FILE" ] && [ -f "$MODEL_FILE" ]; } || { echo "ERROR: No GGUF found on this host"; exit 1; } && ${cmd}`;
@@ -2614,13 +2629,14 @@ function _renderRecipes() {
26142629
const isLocal = !s.host || s.host.toLowerCase() === 'local';
26152630
if (isLocal) {
26162631
s.host = '';
2632+
s.platform = _envState.hostPlatform || '';
26172633
if (_localSeen) return false;
26182634
_localSeen = true;
26192635
}
26202636
return true;
26212637
});
26222638
if (!_localSeen) {
2623-
_es.servers.unshift({ host: '', env: _es.env || 'none', envPath: _es.envPath || '', modelDir: '~/.cache/huggingface/hub' });
2639+
_es.servers.unshift({ host: '', env: _es.env || 'none', envPath: _es.envPath || '', modelDir: '~/.cache/huggingface/hub', platform: _envState.hostPlatform || '' });
26242640
}
26252641
if (_es.remoteHost && !_es.servers.some(s => s.host === _es.remoteHost)) {
26262642
_es.servers.push({ host: _es.remoteHost, env: _es.env || 'none', envPath: _es.envPath || '', modelDir: '~/.cache/huggingface/hub' });

static/js/cookbookRunning.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,7 @@ function _stripStateSecrets(state) {
781781
const safe = { ...state };
782782
if (safe.env && typeof safe.env === 'object') {
783783
const { hfToken, ...env } = safe.env;
784+
delete env.hostPlatform;
784785
safe.env = env;
785786
}
786787
if (Array.isArray(safe.tasks)) safe.tasks = safe.tasks.map(_redactTaskForStorage);
@@ -1673,7 +1674,7 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid
16731674
|| _envState.servers.find(s => s.host === _host) || {};
16741675
const _serverMetaKey = _targetKey || (_hsrv && _serverKey ? _serverKey(_hsrv) : '') || (_host || 'local');
16751676
const _serverMetaName = targetMeta?.serverName || _hsrv.name || (_host ? _host : 'Local');
1676-
const _hplatform = _host ? (_hsrv.platform || '') : (_envState.platform || '');
1677+
const _hplatform = _host ? (_hsrv.platform || '') : (_envState.hostPlatform || '');
16771678
const _replaceTaskId = fields?._replaceTaskId || '';
16781679
if (_replaceTaskId) {
16791680
try {
@@ -1688,7 +1689,6 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid
16881689
}
16891690
} catch {}
16901691
}
1691-
16921692
// Replace any serve already targeting this same host:port — you can't run two
16931693
// servers on one port, so re-serving (or retrying) should stop & remove the
16941694
// old one instead of leaving a dead duplicate behind. (The retry buttons

0 commit comments

Comments
 (0)