feat(settings): add proxy support for model downloads and update checks (#909)

* feat(settings): add proxy support for model downloads and update checks

Add a Proxy page to Settings (above About) with Off / System / Manual
modes. Manual mode supports SOCKS5 and HTTPS (HTTP CONNECT) proxies
with optional username/password auth; the password is stored in
config.json in plaintext like the other settings. System mode honors
the *_proxy environment variables and falls back to the GNOME
org.gnome.system.proxy manual configuration.

The resolved proxy is applied to every in-app outbound path that
fetches models or checks updates: the shared requests download path in
recognition_manager (_stream_model_download, used by the whisper.cpp,
whisper, Vosk, Parakeet and faster-whisper downloaders) and the
release checks in update_checker. A small Test Connection button on
the Proxy page verifies reachability through the configured proxy.

SOCKS5 needs PySocks, added as a runtime dependency; uv.lock, the
requirements/*.txt exports and the Flatpak dependency manifest are
regenerated by just lock (with a fix in sync_flatpak_deps.py for
packages like PySocks that publish more than one universal wheel).

Fixes #655

* fix(proxy): bracket IPv6 hosts, escape test errors, declare pysocks for AUR

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-10-07 17:16:48 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent 442a646a57
commit 775863fd30
17 changed files with 990 additions and 11 deletions
+11 -8
View File
@@ -133,20 +133,23 @@ def choose_artifact(name: str, version: str, digests: set[str]) -> tuple[str, st
if a["packagetype"] == "bdist_wheel" and a["filename"].endswith("-none-any.whl")
]
sdists = [a for a in artifacts if a["packagetype"] == "sdist"]
chosen = wheels or sdists
if not chosen:
candidates = wheels or sdists
if not candidates:
raise SystemExit(
f"{name} {version} publishes neither a universal wheel nor an sdist; "
"the Flatpak cannot build it without an ABI-tagged wheel"
)
picked = chosen[0]
sha = picked["digests"]["sha256"]
if sha not in digests:
# A release can publish more than one universal wheel (PySocks 1.7.1 ships
# py27 and py3 builds); take the first whose digest uv actually locked.
picked = next((a for a in candidates if a["digests"]["sha256"] in digests), None)
if picked is None:
first = candidates[0]
raise SystemExit(
f"{name} {version}: PyPI serves {picked['filename']} with digest "
f"{sha}, which is not among the hashes uv locked. Re-run `just lock`."
f"{name} {version}: PyPI serves {first['filename']} with digest "
f"{first['digests']['sha256']}, which is not among the hashes uv "
"locked. Re-run `just lock`."
)
return picked["url"], sha
return picked["url"], picked["digests"]["sha256"]
def artifact_name(filename: str) -> str: