models: add China download mirrors

Prefer ghproxy.net for model manifests and hf-mirror.com for Hugging Face artifacts, with automatic fallback to the original URLs.
This commit is contained in:
nrdr integration
2026-09-10 20:29:04 +08:00
parent ebaba75710
commit 4f782b4ac7
2 changed files with 107 additions and 61 deletions
+35 -24
View File
@@ -141,6 +141,11 @@ class ModelFetcher:
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json"
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v25.json"
# GitHub raw is frequently unreachable from mainland China. Keep the original
# URLs authoritative while trying this proxy first for manifests.
MODEL_URL_MIRROR = "https://ghproxy.net/" + MODEL_URL
MODEL_URL_CHESTNUT_MIRROR = "https://ghproxy.net/" + MODEL_URL_CHESTNUT
MODEL_SOURCES = {
"qcom": (MODEL_URL, ""),
"chestnut": (MODEL_URL_CHESTNUT, "_Chestnut"),
@@ -163,37 +168,43 @@ class ModelFetcher:
def active_source(chestnut_present: bool) -> str:
return "chestnut" if chestnut_present else "qcom"
@classmethod
def model_urls(cls, source: str) -> tuple[str, ...]:
model_url, _ = cls.MODEL_SOURCES[source]
mirror = cls.MODEL_URL_MIRROR if source == "qcom" else cls.MODEL_URL_CHESTNUT_MIRROR
return (mirror, model_url)
def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None:
"""Fetches fresh model data from remote and updates cache.
Returns None on transport errors. Raises on 404 and other fatal HTTP errors.
Returns None when every mirror and the original URL fail.
"""
model_url, _ = self.MODEL_SOURCES[source]
try:
response = requests.get(model_url, timeout=10)
for model_url in self.model_urls(source):
try:
response = requests.get(model_url, timeout=10)
# Explicitly handle 404 differently
if response.status_code == 404:
cloudlog.error(f"Models URL returned 404 Not Found: {model_url}")
raise HTTPError(f"404 Not Found: {model_url}", response=response)
# Explicitly handle 404 differently
if response.status_code == 404:
cloudlog.error(f"Models URL returned 404 Not Found: {model_url}")
raise HTTPError(f"404 Not Found: {model_url}", response=response)
# Raise for any other 4xx/5xx
response.raise_for_status()
# Raise for any other 4xx/5xx
response.raise_for_status()
json_data = response.json()
parsed = self.model_parser.parse_models(json_data)
if parsed:
self.model_caches[source].set(json_data)
cloudlog.debug(f"Successfully updated models cache for {source}")
return parsed
json_data = response.json()
parsed = self.model_parser.parse_models(json_data)
if parsed:
self.model_caches[source].set(json_data)
cloudlog.debug(f"Successfully updated models cache for {source} via {model_url}")
return parsed
except ConnectionError as e:
cloudlog.warning(f"DNS/connection error while fetching models: {e}")
except SSLError as e:
cloudlog.warning(f"SSL error while fetching models: {e}")
except RequestException as e:
cloudlog.warning(f"Request transport error while fetching models: {e}")
except Exception as e:
cloudlog.exception(f"Unexpected error fetching models: {e}")
except ConnectionError as e:
cloudlog.warning(f"DNS/connection error while fetching models via {model_url}: {e}")
except SSLError as e:
cloudlog.warning(f"SSL error while fetching models via {model_url}: {e}")
except RequestException as e:
cloudlog.warning(f"Request transport error while fetching models via {model_url}: {e}")
except Exception as e:
cloudlog.exception(f"Unexpected error fetching models via {model_url}: {e}")
return None
+72 -37
View File
@@ -31,6 +31,16 @@ class DownloadCancelled(Exception):
class ModelManagerSP:
"""Manages model downloads and status reporting"""
HF_MIRROR = os.environ.get("HF_ENDPOINT", "https://hf-mirror.com").rstrip("/")
@classmethod
def download_urls(cls, url: str) -> tuple[str, ...]:
"""Prefer the Hugging Face mirror, then fall back to the original URL."""
if url.startswith("https://huggingface.co/"):
mirror = cls.HF_MIRROR + url[len("https://huggingface.co"):]
return (mirror, url)
return (url,)
def __init__(self):
self.params = Params()
self.model_fetcher = ModelFetcher(self.params)
@@ -82,32 +92,45 @@ class ModelManagerSP:
return max(1, int(eta)) # Return at least 1 second if download is ongoing
async def _download_file(self, url: str, path: str, model) -> None:
"""Downloads a file with progress tracking"""
"""Downloads a file with progress tracking and mirror fallback"""
self._download_start_times[model.fileName] = time.monotonic()
last_error = None
with requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: # noqa: ASYNC210
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
bytes_downloaded = 0
for candidate_url in self.download_urls(url):
try:
with requests.get(candidate_url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: # noqa: ASYNC210
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
bytes_downloaded = 0
with open(path, 'wb') as f: # noqa: ASYNC230
for chunk in response.iter_content(chunk_size=self._chunk_size): # type: bytes
f.write(chunk)
bytes_downloaded += len(chunk)
with open(path, 'wb') as f: # noqa: ASYNC230
for chunk in response.iter_content(chunk_size=self._chunk_size): # type: bytes
f.write(chunk)
bytes_downloaded += len(chunk)
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
if total_size > 0:
progress = (bytes_downloaded / total_size) * 100
model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
model.downloadProgress.progress = progress
model.downloadProgress.eta = self._calculate_eta(model.fileName, progress)
self._sync_artifact_progress(model)
self._report_status()
if total_size > 0:
progress = (bytes_downloaded / total_size) * 100
model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
model.downloadProgress.progress = progress
model.downloadProgress.eta = self._calculate_eta(model.fileName, progress)
self._sync_artifact_progress(model)
self._report_status()
# Clean up start time after download completes
del self._download_start_times[model.fileName]
self._download_start_times.pop(model.fileName, None)
return
except DownloadCancelled:
self._download_start_times.pop(model.fileName, None)
raise
except Exception as exc:
last_error = exc
cloudlog.warning(f"Model download failed via {candidate_url}: {exc}")
self._download_start_times.pop(model.fileName, None)
if last_error is not None:
raise last_error
async def _download_chunked(self, base_url: str, base_path: str, artifact, skip: frozenset[int] | set[int] = frozenset()) -> None:
from openpilot.common.file_chunker import get_chunk_name, get_manifest_path
@@ -128,23 +151,35 @@ class ModelManagerSP:
continue
chunk_url = get_chunk_name(base_url, i, num_chunks)
chunk_path = get_chunk_name(base_path, i, num_chunks)
chunk_downloaded = 0
with session.get(chunk_url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
response.raise_for_status()
chunk_size = int(response.headers.get("content-length", 0))
with open(chunk_path, 'wb') as f: # noqa: ASYNC230
for data in response.iter_content(chunk_size=self._chunk_size):
f.write(data)
chunk_downloaded += len(data)
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
intra = chunk_downloaded / max(chunk_size, 1)
progress = min(99.0, ((completed + intra) / num_chunks) * 100)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
artifact.downloadProgress.progress = progress
artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress)
self._sync_artifact_progress(artifact)
self._report_status()
chunk_error = None
for candidate_url in self.download_urls(chunk_url):
try:
chunk_downloaded = 0
with session.get(candidate_url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
response.raise_for_status()
chunk_size = int(response.headers.get("content-length", 0))
with open(chunk_path, 'wb') as f: # noqa: ASYNC230
for data in response.iter_content(chunk_size=self._chunk_size):
f.write(data)
chunk_downloaded += len(data)
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
intra = chunk_downloaded / max(chunk_size, 1)
progress = min(99.0, ((completed + intra) / num_chunks) * 100)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
artifact.downloadProgress.progress = progress
artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress)
self._sync_artifact_progress(artifact)
self._report_status()
chunk_error = None
break
except DownloadCancelled:
raise
except Exception as exc:
chunk_error = exc
cloudlog.warning(f"Model chunk download failed via {candidate_url}: {exc}")
if chunk_error is not None:
raise chunk_error
completed += 1
with open(manifest_path, 'w') as f: # noqa: ASYNC230