Skip to content

Commit 4f70435

Browse files
committed
fix(cookbook): prefer native llama-server on local Windows
1 parent ffd0aaf commit 4f70435

5 files changed

Lines changed: 78 additions & 18 deletions

File tree

routes/cookbook_helpers.py

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

routes/cookbook_routes.py

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

76+
def _client_host_platform() -> str:
77+
return "windows" if IS_WINDOWS else ""
78+
7679
def _decrypt_secret(value: str | None) -> str:
7780
if not value:
7881
return ""
@@ -226,11 +229,15 @@ def _state_for_client(state):
226229
"""Return cookbook state without raw secrets for browser clients."""
227230
_strip_task_secrets(state)
228231
env = state.get("env") if isinstance(state, dict) else None
232+
if isinstance(state, dict) and not isinstance(env, dict):
233+
env = {}
234+
state["env"] = env
229235
if isinstance(env, dict):
230236
token = _decrypt_secret(env.get("hfToken"))
231237
env.pop("hfToken", None)
232238
env["hfTokenConfigured"] = bool(token)
233239
env["hfTokenMasked"] = _mask_secret(token)
240+
env["hostPlatform"] = _client_host_platform()
234241
return state
235242

236243
def _state_for_storage(state, on_disk=None):
@@ -249,6 +256,7 @@ def _state_for_storage(state, on_disk=None):
249256
env.pop("hfToken", None)
250257
env.pop("hfTokenMasked", None)
251258
env.pop("hfTokenConfigured", None)
259+
env.pop("hostPlatform", None)
252260
return state
253261

254262
def _load_stored_hf_token() -> str:
@@ -1387,7 +1395,8 @@ async def model_serve(request: Request, req: ServeRequest):
13871395
runner_lines.append(_HF_TOKEN_STATUS_SNIPPET)
13881396
handled_ollama_serve = False
13891397
# Auto-install inference engine if missing
1390-
if "llama_cpp" in req.cmd or "llama-server" in req.cmd:
1398+
local_windows_llama_cmd = local_windows and ("llama_cpp" in req.cmd or "llama-server" in req.cmd)
1399+
if ("llama_cpp" in req.cmd or "llama-server" in req.cmd) and not local_windows_llama_cmd:
13911400
# Prefer the NATIVE llama-server binary — its minja templating
13921401
# renders modern GGUF chat templates that the Python bindings'
13931402
# Jinja2 rejects (do_tojson ensure_ascii). Build it once from
@@ -2110,8 +2119,8 @@ async def get_cookbook_state(request: Request):
21102119
try:
21112120
return _state_for_client(json.loads(_cookbook_state_path.read_text(encoding="utf-8")))
21122121
except Exception:
2113-
return {}
2114-
return {}
2122+
return _state_for_client({})
2123+
return _state_for_client({})
21152124

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

static/js/cookbook.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function _platformIcon(platform) {
7373
return '';
7474
}
7575

76-
export let _envState = { env: 'none', envPath: '', hfToken: '', hfTokenConfigured: false, hfTokenMasked: '', gpus: '', remoteHost: '', servers: [], modelPaths: [], platform: '', defaultServer: '' };
76+
export let _envState = { env: 'none', envPath: '', hfToken: '', hfTokenConfigured: false, hfTokenMasked: '', gpus: '', remoteHost: '', servers: [], modelPaths: [], platform: '', hostPlatform: '', defaultServer: '' };
7777
let _lastCacheHostVal = null;
7878
let _cookbookOpeningSpinners = [];
7979
export function _lastCacheHost() { return _lastCacheHostVal; }
@@ -210,8 +210,13 @@ function _getPort(hostOrTask) {
210210

211211
/** Get platform for a given host (or task object). Returns 'windows', 'termux', 'linux', or '' */
212212
export function _getPlatform(hostOrTask) {
213-
if (!hostOrTask) return _envState.platform || '';
214-
if (typeof hostOrTask === 'object') return hostOrTask.platform || _getPlatform(hostOrTask.remoteServerKey || hostOrTask.remoteHost);
213+
if (hostOrTask === 'local') return _envState.hostPlatform || '';
214+
if (!hostOrTask) return _envState.remoteHost ? (_envState.platform || '') : (_envState.hostPlatform || '');
215+
if (typeof hostOrTask === 'object') {
216+
const taskHost = hostOrTask.remoteServerKey || hostOrTask.remoteHost || '';
217+
if (!taskHost || taskHost === 'local') return _envState.hostPlatform || '';
218+
return hostOrTask.platform || _getPlatform(taskHost);
219+
}
215220
const selected = hostOrTask === _envState.remoteHost ? _selectedServer() : null;
216221
const srv = selected || _serverByVal(hostOrTask);
217222
return srv?.platform || '';
@@ -532,25 +537,27 @@ export function _buildServeCmd(f, modelName, backend) {
532537
// GPU list — read from gpus (button strip); fall back to gpu_id for
533538
// backward-compat with older saved presets that pre-date the removal.
534539
const gpuId = (f.gpus || f.gpu_id || '').toString().trim();
535-
const py = _isWindows() ? 'python' : 'python3';
540+
const _isWin = _isWindows();
541+
const _localWindows = _isWin && !_envState.remoteHost;
542+
const py = _isWin ? 'python' : 'python3';
536543
// CPU-only serve (-ngl 0): drop the GPU-only flags, otherwise the command
537544
// mixes "zero GPU layers" with CUDA unified-memory + flash-attn and fails to
538545
// start (issue #1291). Only affects the ngl=0 path; GPU serving is unchanged.
539546
const _cpuOnly = String(f.ngl).trim() === '0';
540547
const lcPrefix = (() => {
541548
let p = '';
542-
if (f.unified_mem && !_cpuOnly && !_isWindows()) p += `GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 `;
543-
if (gpuId && !_isWindows()) p += `CUDA_VISIBLE_DEVICES=${gpuId} `;
549+
if (f.unified_mem && !_cpuOnly && (!_isWin || _localWindows)) p += `GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 `;
550+
if (gpuId && (!_isWin || _localWindows)) p += `CUDA_VISIBLE_DEVICES=${gpuId} `;
544551
return p;
545552
})();
546-
if (f.unified_mem && !_cpuOnly && _isWindows()) cmd += `$env:GGML_CUDA_ENABLE_UNIFIED_MEMORY="1"; `;
547-
if (gpuId && _isWindows()) cmd += `$env:CUDA_VISIBLE_DEVICES="${gpuId}"; `;
548-
if (!_isWindows()) {
553+
if (f.unified_mem && !_cpuOnly && _isWin && !_localWindows) cmd += `$env:GGML_CUDA_ENABLE_UNIFIED_MEMORY="1"; `;
554+
if (gpuId && _isWin && !_localWindows) cmd += `$env:CUDA_VISIBLE_DEVICES="${gpuId}"; `;
555+
if (!_isWin) {
549556
// Resolve GGUF path once, fail loudly if nothing matched (prevents
550557
// `--model ""` which causes confusing downstream errors).
551558
cmd += `MODEL_FILE=${ggufPath} && { [ -n "$MODEL_FILE" ] && [ -f "$MODEL_FILE" ]; } || { echo "ERROR: No GGUF found on this host. Either download the model here, or switch to the server where it's cached."; exit 1; } && `;
552559
}
553-
const modelArg = _isWindows() ? `"${ggufPath}"` : `"$MODEL_FILE"`;
560+
const modelArg = _isWin ? `"${ggufPath}"` : `"$MODEL_FILE"`;
554561
// Prefer the native llama-server binary on Linux — its minja templating
555562
// renders modern GGUF chat templates that the Python bindings' Jinja2
556563
// rejects (do_tojson ensure_ascii). Fall back to llama_cpp.server.
@@ -613,11 +620,16 @@ export function _buildServeCmd(f, modelName, backend) {
613620
// llama-cpp-python takes the projector via --clip_model_path.
614621
_lcpExtra += ` --clip_model_path "${f._mmproj_path}"`;
615622
}
623+
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}`;
616624
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}`;
617-
if (_isWindows()) {
625+
if (_localWindows) {
626+
// Local Windows serve is launched through Git Bash, so use the native
627+
// llama-server shape and let PATH resolve the CUDA Release wrapper.
628+
cmd += _lcServer;
629+
} else if (_isWin) {
618630
cmd += _lcpServer;
619631
} else {
620-
cmd += `${lcPrefix}llama-server --model ${modelArg} --host 0.0.0.0 --port ${f.port || '8080'} -ngl ${f.ngl || '99'} -c ${f.ctx || '8192'}${_lcExtra}`;
632+
cmd += _lcServer;
621633
cmd += ` || ${_lcpServer}`;
622634
}
623635
} else if (backend === 'ollama') {
@@ -2099,13 +2111,14 @@ function _renderRecipes() {
20992111
const isLocal = !s.host || s.host.toLowerCase() === 'local';
21002112
if (isLocal) {
21012113
s.host = '';
2114+
s.platform = _envState.hostPlatform || _envState.platform || '';
21022115
if (_localSeen) return false;
21032116
_localSeen = true;
21042117
}
21052118
return true;
21062119
});
21072120
if (!_localSeen) {
2108-
_es.servers.unshift({ host: '', env: _es.env || 'none', envPath: _es.envPath || '', modelDir: '~/.cache/huggingface/hub' });
2121+
_es.servers.unshift({ host: '', env: _es.env || 'none', envPath: _es.envPath || '', modelDir: '~/.cache/huggingface/hub', platform: _envState.hostPlatform || _envState.platform || '' });
21092122
}
21102123
if (_es.remoteHost && !_es.servers.some(s => s.host === _es.remoteHost)) {
21112124
_es.servers.push({ host: _es.remoteHost, env: _es.env || 'none', envPath: _es.envPath || '', modelDir: '~/.cache/huggingface/hub' });

static/js/cookbookRunning.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ function _stripStateSecrets(state) {
667667
if (safe.env && typeof safe.env === 'object') {
668668
const { hfToken, ...env } = safe.env;
669669
if (hfToken) env.hfToken = hfToken;
670+
delete env.hostPlatform;
670671
safe.env = env;
671672
}
672673
if (Array.isArray(safe.tasks)) safe.tasks = safe.tasks.map(_stripTaskSecrets);

tests/test_cookbook_cpu_only_serve.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Regression guard for issue #1291 CPU-only serve still emitted GPU-only flags.
1+
"""Regression guard for issue #1291 - CPU-only serve still emitted GPU-only flags.
22
33
The llama.cpp serve command builder (static/js/cookbook.js) added
44
`--flash-attn on` and exported `GGML_CUDA_ENABLE_UNIFIED_MEMORY=1` from
@@ -16,6 +16,7 @@
1616

1717
SRC = Path(__file__).resolve().parent.parent / "static/js/cookbook.js"
1818
SERVE_SRC = Path(__file__).resolve().parent.parent / "static/js/cookbookServe.js"
19+
ROOT = SRC.parent.parent.parent
1920

2021

2122
def test_cpu_only_drops_gpu_only_flags():
@@ -51,3 +52,39 @@ def test_windows_diffusers_uses_python_not_python3():
5152
assert "const diffusersPy = _isWindows() ? 'python' : _py3Bin;" in text
5253
assert "cmd += `${diffusersPy} scripts/diffusion_server.py" in text
5354
assert "cmd += `python3 scripts/diffusion_server.py" not in text
55+
56+
57+
def test_local_windows_platform_comes_from_backend_host_state():
58+
text = SRC.read_text(encoding="utf-8")
59+
routes = (ROOT / "routes/cookbook_routes.py").read_text(encoding="utf-8")
60+
running = (SRC.parent / "cookbookRunning.js").read_text(encoding="utf-8")
61+
62+
assert "hostPlatform" in text
63+
assert "navigator.platform" not in text
64+
assert "hostOrTask === 'local'" in text
65+
assert "if (hostOrTask === 'local') return _envState.hostPlatform || '';" in text
66+
assert "return _envState.hostPlatform || _envState.platform || ''" not in text
67+
assert 'return "windows" if IS_WINDOWS else ""' in routes
68+
assert 'env["hostPlatform"] = _client_host_platform()' in routes
69+
assert "return _state_for_client({})" in routes
70+
assert 'env.pop("hostPlatform", None)' in routes
71+
assert "delete env.hostPlatform;" in running
72+
73+
74+
def test_local_windows_llamacpp_prefers_native_llama_server():
75+
text = SRC.read_text(encoding="utf-8")
76+
helpers = (ROOT / "routes/cookbook_helpers.py").read_text(encoding="utf-8")
77+
78+
assert "const _localWindows = _isWin && !_envState.remoteHost;" in text
79+
assert "const gpuId = (f.gpus || f.gpu_id || '').toString().trim();" in text
80+
assert "const _lcServer = `${lcPrefix}llama-server --model" in text
81+
assert "if (_localWindows) {" in text
82+
assert "cmd += _lcServer;" in text
83+
assert '"llama-server.exe"' in helpers
84+
85+
86+
def test_local_windows_llama_server_skips_source_bootstrap():
87+
routes = (ROOT / "routes/cookbook_routes.py").read_text(encoding="utf-8")
88+
89+
assert 'local_windows_llama_cmd = local_windows and ("llama_cpp" in req.cmd or "llama-server" in req.cmd)' in routes
90+
assert 'if ("llama_cpp" in req.cmd or "llama-server" in req.cmd) and not local_windows_llama_cmd:' in routes

0 commit comments

Comments
 (0)