# Qwen local companion files: 4 November 2026

These files reproduce the local text, image, web-search and completion-recovery setup described at https://blog.slavichek.uk/articles/qwen35-2b-vostro-local-llm/ . All instructions are in English. The original Russian prompt and output in measurements.json are unchanged benchmark sample data.

This package contains no model weights, runtime binaries, credentials, private hostnames, chat identifiers, screenshots or source photos. Model inference runs locally. Optional Exa search sends queries to its hosted service; the keyless endpoint has provider limits and no paid fallback is configured.

## Contents

- qwen-local.service: portable systemd user unit using %h/local-llm, loopback port 8092, the matching vision projector, two inference threads, a 4096-token context and a 3 GiB memory limit.
- ui.json: native MCP browser defaults. customJson is empty so native tool switches keep working.
- web-search/bootstrap.js: the tested local adapter, source-link footer and small-model request policy. It enables only the readonly Exa search permission, bounds tool text, honors disabled tools, keeps normal photo descriptions local and caps explicit thinking.
- reliability/prepare_ui.py: the tested b12032 budget patch. It reads original assets from runtime/build/tools/ui/dist and writes a separate bundle plus the bootstrap into runtime/webui. The loader must first be added to index.html as shown below.
- reliability/stream-retention.patch: the complete one-line private patch against llama.cpp commit 8642541b255bfb5dc39cd351a178be09013a55d6. It changes completed-stream retention from 300 to 86400 seconds.
- measure.py: unchanged standard-library text/API timing client; it does not run MCP tools or enable background replay. Incomplete streams fail instead of producing a timing result.
- measurements.json: original text-only observation from 1 November.
- measurements-20261104.json: sanitized photo, search, recovery and resource observations. Single runs under varying load are not controlled performance averages.
- SHA256SUMS: checksums for every other file in this package.

## Fresh installation

Follow the article's pinned dependency, model, projector and CPU-build commands first. Apply stream-retention.patch before compiling. Stop an existing Qwen service before replacing its executable or mapped libraries. No changes to other services are needed.

From the directory containing the extracted versioned package, use the same shell for these commands:

```bash
llm_root="$HOME/local-llm"
bundle_dir="$PWD/qwen-local-files-20261104"

# Check the extracted package before copying its files.
(cd "$bundle_dir" && sha256sum --check SHA256SUMS)

mkdir -p "$llm_root/config"
cp "$bundle_dir/ui.json" "$llm_root/config/ui.json"
cp "$bundle_dir/measure.py" "$llm_root/measure.py"
cp -a "$bundle_dir/web-search" "$llm_root/"
cp -a "$bundle_dir/reliability" "$llm_root/"
```

The pinned build accepts verified assets supplied in runtime/llama.cpp/tools/ui/dist. Make the immutable working copy expected by the UI helper, then a separate served copy. Remove only the copied service worker, never browser history:

```bash
mkdir -p "$llm_root/runtime/build/tools/ui/dist" "$llm_root/runtime/webui"
cp -a "$llm_root/runtime/llama.cpp/tools/ui/dist/." \
  "$llm_root/runtime/build/tools/ui/dist/"
cp -a "$llm_root/runtime/build/tools/ui/dist/." \
  "$llm_root/runtime/webui/"
rm -f "$llm_root/runtime/webui/sw.js"

python3 - "$llm_root/runtime/webui/index.html" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
text = path.read_text()
loader = '<script src="/local-qwen-web-search.js?v=10"></script>'
if 'local-qwen-web-search.js' not in text:
    if text.count('<head>') != 1:
        raise SystemExit('Unexpected UI index; inspect it before continuing')
    path.write_text(text.replace('<head>', '<head>\n' + loader, 1))
PY

python3 "$llm_root/reliability/prepare_ui.py"
```

Keep the original b12032 files unchanged. The helper deliberately refuses an unexpected budget mapping rather than guessing how to patch a different UI release. Node.js is not required.

Install the supplied unit only after both verified model files, the built server, config/ui.json and runtime/webui exist. Copy it to $HOME/.config/systemd/user/qwen-local.service, verify it with systemd-analyze --user verify, reload the user manager and enable/start the unit as shown in the article. Do not install a second copy under another service name.

## Upgrade the earlier text-only installation

Keep your existing browser data. Download this versioned package into a new folder. Verify the pinned runtime commit and download/check the projector from the same model revision. Finish any active chat, then stop only qwen-local.service before applying the retention patch or rebuilding.

Apply the patch only once. git apply --check confirms it can be applied; git apply --reverse --check confirms it is already present. An unexpected failure means inspect the source changes rather than resetting the repository. Rebuild the server, run the UI-copy/loader/helper steps above, install the updated unit and browser defaults, then daemon-reload and start Qwen again. No new model-language weights or system package upgrade are needed for this update.

The native bootstrap migrates the previous static empty-tool override once while retaining history and unrelated preferences. Refresh the page after installation; start a new conversation if an existing conversation has its own disabled-tool policy. Do not clear site storage or IndexedDB. Rebuilding/restarting clears temporary server replay; completed replies already saved in browser history remain.

## Use and check

Open http://127.0.0.1:8092/ on the machine. Check /health for status ok and /props for modalities.vision=true. Use + > Add files for a photo. Reasoning Off is fastest for a simple description. Local budgets are Low 64, Medium 96 and High/Max 128 thinking tokens; the adapter also accounts for smaller positive output limits.

Use /search followed by a query for reliable web search. Browser date/info tools are disabled by default to keep prompts small. Native search switches still work, and only search has a saved permission. A plain question with a new image suppresses automatic Exa search; an explicit /search request can still use it. The Sources footer comes from actual tool results and is separate from model-token counts.

Already submitted streaming completions continue after a disconnect. Reopen the same conversation in the same browser and origin. Completed buffers remain for up to 24 hours while the service runs, with a 4 MiB ring per session. Explicit Stop, a newer completion for the same conversation, service restart or expiry removes that replay. There is no additional aggregate-session cap. Browser-managed MCP workflows need an open browser to execute their tools; this is not a general background-agent scheduler.

To measure a new English text request:

```bash
python3 "$HOME/local-llm/measure.py" "Name the capital of France"
```

The original 4.601-second first text and 26.554-second full stream describe one Russian prompt with 21 of 39 input tokens cached. The updated photo test used a 300x238 CCTV crop and completed in 222.720 seconds with the browser closed. A further 365-second wait and reopen recovered the exact answer without another inference. Model decode rates exclude prompt processing; generated counts in the High test include reasoning. Do not compare runs as though image size, prompts, cache and host load were identical.

For remote access, keep the origin loopback-only and protect the whole hostname, including its API, through your existing HTTPS tunnel and authentication policy. Do not publish the raw model port or put a management credential into the UI. Local inference does not add video generation.
