mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
perf(modelscope): fetch a repository's model card once per run
A collection repository publishes many model files under a single source id, but enrichment re-read the README and the model-detail payload for every one of them: eight checkpoints meant sixteen HTTP requests, each detail payload being 10-22 KB of JSON. Add `ModelSourceCache`, created by `execute_skill()` for the duration of a run and passed to the provider through a new optional `cache` argument on `fetch_model_card_context()`. The agent caches the README (repository-wide and provider-agnostic), and ModelScope caches its detail payload under a provider-namespaced key. Only successful reads are memoised, so a transient failure is still retried for the next file, and the per-file selection is redone from the cached payload so a checkpoint never inherits a sibling's example images. Nothing is retained across runs — a model card can change at any time — and download URLs are not routed through the cache. Measured over the eight checkpoints of one ModelScope repository: 16 requests before, 2 after. To keep the two concerns separable, `_build_card_context()` now turns a detail payload into a `ModelCardContext` as a pure function.
This commit is contained in:
@@ -109,6 +109,13 @@ returns a `ModelCardContext`. Example images are matched to the model's
|
|||||||
Sites with no such extras inherit an empty context, and the pipeline behaves
|
Sites with no such extras inherit an empty context, and the pipeline behaves
|
||||||
exactly as before.
|
exactly as before.
|
||||||
|
|
||||||
|
The README and the repository metadata describe the whole repository, not one
|
||||||
|
file, so `execute_skill()` creates a `ModelSourceCache` for the duration of a
|
||||||
|
run and passes it down. Enriching the eight checkpoints of one ModelScope
|
||||||
|
repository costs two HTTP requests instead of sixteen; only the per-file
|
||||||
|
selection is redone for each file. Nothing is cached across runs, and download
|
||||||
|
URLs never go through it.
|
||||||
|
|
||||||
#### Deterministic data is applied whether or not an LLM is configured
|
#### Deterministic data is applied whether or not an LLM is configured
|
||||||
|
|
||||||
`AgentService._load_source_card()` runs for every source-backed enrichment, and
|
`AgentService._load_source_card()` runs for every source-backed enrichment, and
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from ...config import config
|
|||||||
from ..llm_service import LLMService
|
from ..llm_service import LLMService
|
||||||
from ..model_sources import (
|
from ..model_sources import (
|
||||||
ModelCardContext,
|
ModelCardContext,
|
||||||
|
ModelSourceCache,
|
||||||
get_source,
|
get_source,
|
||||||
resolve_source_ref,
|
resolve_source_ref,
|
||||||
source_label,
|
source_label,
|
||||||
@@ -259,6 +260,11 @@ class AgentService:
|
|||||||
llm = await self._ensure_llm()
|
llm = await self._ensure_llm()
|
||||||
llm_configured = llm.is_configured() if skill.llm_required else True
|
llm_configured = llm.is_configured() if skill.llm_required else True
|
||||||
|
|
||||||
|
# A collection repository holds many model files under one source id;
|
||||||
|
# this memo keeps the README and the repository metadata from being
|
||||||
|
# re-fetched once per file. It lives for this run only.
|
||||||
|
source_cache = ModelSourceCache()
|
||||||
|
|
||||||
for model_path in model_paths:
|
for model_path in model_paths:
|
||||||
model_filename = os.path.basename(model_path)
|
model_filename = os.path.basename(model_path)
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -288,7 +294,7 @@ class AgentService:
|
|||||||
# or not an LLM is available: a user without a key still gets
|
# or not an LLM is available: a user without a key still gets
|
||||||
# the author summary, the example images and the tags.
|
# the author summary, the example images and the tags.
|
||||||
source_vars, source_context = await self._load_source_card(
|
source_vars, source_context = await self._load_source_card(
|
||||||
model_path, metadata,
|
model_path, metadata, cache=source_cache,
|
||||||
)
|
)
|
||||||
resolved_base_model = ""
|
resolved_base_model = ""
|
||||||
if skill_name == "enrich_hf_metadata" and not (
|
if skill_name == "enrich_hf_metadata" and not (
|
||||||
@@ -423,13 +429,22 @@ class AgentService:
|
|||||||
return "\n".join(f"- {m}" for m in models)
|
return "\n".join(f"- {m}" for m in models)
|
||||||
|
|
||||||
async def _load_source_card(
|
async def _load_source_card(
|
||||||
self, model_path: str, metadata: Dict[str, Any]
|
self,
|
||||||
|
model_path: str,
|
||||||
|
metadata: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
cache: Optional[ModelSourceCache] = None,
|
||||||
) -> tuple[Dict[str, Any], ModelCardContext]:
|
) -> tuple[Dict[str, Any], ModelCardContext]:
|
||||||
"""Fetch the model card and site-published extras for one model.
|
"""Fetch the model card and site-published extras for one model.
|
||||||
|
|
||||||
Runs for every source-backed enrichment regardless of LLM
|
Runs for every source-backed enrichment regardless of LLM
|
||||||
availability, because everything it returns is deterministic data that
|
availability, because everything it returns is deterministic data that
|
||||||
should be applied even without a configured provider.
|
should be applied even without a configured provider.
|
||||||
|
|
||||||
|
*cache* is the per-run memo created by :meth:`execute_skill`. The
|
||||||
|
README is repository-wide, so it is fetched once per source id; only
|
||||||
|
successful reads are memoised, leaving a transient failure to be
|
||||||
|
retried for the next file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
variables: Dict[str, Any] = {
|
variables: Dict[str, Any] = {
|
||||||
@@ -450,11 +465,18 @@ class AgentService:
|
|||||||
|
|
||||||
raw_basename = os.path.splitext(os.path.basename(model_path))[0]
|
raw_basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||||
variables["asset_base_url"] = source.asset_base_url(ref.source_id)
|
variables["asset_base_url"] = source.asset_base_url(ref.source_id)
|
||||||
|
|
||||||
|
cache_key = f"{ref.platform}:{ref.source_id}"
|
||||||
|
readme = cache.readmes.get(cache_key) if cache is not None else None
|
||||||
|
if readme is None:
|
||||||
readme = await source.fetch_model_card(ref.source_id)
|
readme = await source.fetch_model_card(ref.source_id)
|
||||||
|
if cache is not None and readme:
|
||||||
|
cache.readmes[cache_key] = readme
|
||||||
|
|
||||||
# Sites such as ModelScope keep part of the model card outside the
|
# Sites such as ModelScope keep part of the model card outside the
|
||||||
# README (author summary, curated tags, per-file example images).
|
# README (author summary, curated tags, per-file example images).
|
||||||
card_context = await source.fetch_model_card_context(
|
card_context = await source.fetch_model_card_context(
|
||||||
ref.source_id, os.path.basename(model_path)
|
ref.source_id, os.path.basename(model_path), cache=cache,
|
||||||
)
|
)
|
||||||
variables["source_description"] = card_context.description
|
variables["source_description"] = card_context.description
|
||||||
variables["source_base_model"] = card_context.base_model
|
variables["source_base_model"] = card_context.base_model
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .base import (
|
|||||||
HTTP_TIMEOUT,
|
HTTP_TIMEOUT,
|
||||||
ModelCardContext,
|
ModelCardContext,
|
||||||
ModelSource,
|
ModelSource,
|
||||||
|
ModelSourceCache,
|
||||||
ModelSourceError,
|
ModelSourceError,
|
||||||
SourceRef,
|
SourceRef,
|
||||||
USER_AGENT,
|
USER_AGENT,
|
||||||
@@ -48,6 +49,7 @@ __all__ = [
|
|||||||
"LEGACY_HF_URL_FIELD",
|
"LEGACY_HF_URL_FIELD",
|
||||||
"ModelCardContext",
|
"ModelCardContext",
|
||||||
"ModelSource",
|
"ModelSource",
|
||||||
|
"ModelSourceCache",
|
||||||
"ModelSourceError",
|
"ModelSourceError",
|
||||||
"HuggingFaceSource",
|
"HuggingFaceSource",
|
||||||
"ModelScopeSource",
|
"ModelScopeSource",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Iterable, Optional
|
from typing import Any, Dict, Iterable, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
@@ -125,6 +125,26 @@ class ModelSourceError(Exception):
|
|||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSourceCache:
|
||||||
|
"""Per-run memo shared between the agent pipeline and a model source.
|
||||||
|
|
||||||
|
A collection repository publishes many model files under a single source
|
||||||
|
id, so enriching each file re-fetches the same README and the same
|
||||||
|
repository metadata. One cache is created per enrichment run and thrown
|
||||||
|
away afterwards: nothing is retained across runs (a model card can change
|
||||||
|
at any time), and download URLs are never routed through it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
#: Provider-agnostic: ``"<platform>:<source_id>"`` → raw README text.
|
||||||
|
self.readmes: Dict[str, str] = {}
|
||||||
|
#: Provider-owned scratch space. Keys must be namespaced by the
|
||||||
|
#: provider (``(platform, kind, source_id)``) so two providers can
|
||||||
|
#: never collide. Only successful results should be stored, so a
|
||||||
|
#: transient failure is still retried for the next file.
|
||||||
|
self.provider: Dict[Any, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
#: Repository ids are always exactly ``owner/name``. Components may contain
|
#: Repository ids are always exactly ``owner/name``. Components may contain
|
||||||
#: dots (``black-forest-labs/FLUX.1-dev``) but must not be empty, ``.`` / ``..``,
|
#: dots (``black-forest-labs/FLUX.1-dev``) but must not be empty, ``.`` / ``..``,
|
||||||
#: or start with a dot - the id is used as a path segment on disk.
|
#: or start with a dot - the id is used as a path segment on disk.
|
||||||
@@ -283,7 +303,11 @@ class ModelSource:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def fetch_model_card_context(
|
async def fetch_model_card_context(
|
||||||
self, source_id: str, filename: str = ""
|
self,
|
||||||
|
source_id: str,
|
||||||
|
filename: str = "",
|
||||||
|
*,
|
||||||
|
cache: Optional["ModelSourceCache"] = None,
|
||||||
) -> ModelCardContext:
|
) -> ModelCardContext:
|
||||||
"""Return the card extras the site keeps outside the README.
|
"""Return the card extras the site keeps outside the README.
|
||||||
|
|
||||||
@@ -292,6 +316,10 @@ class ModelSource:
|
|||||||
model card is fully described by :meth:`fetch_model_card` need no
|
model card is fully described by :meth:`fetch_model_card` need no
|
||||||
override and inherit this empty context.
|
override and inherit this empty context.
|
||||||
|
|
||||||
|
*cache* is an optional per-run memo (see :class:`ModelSourceCache`)
|
||||||
|
that lets a provider avoid re-fetching repository-wide data for every
|
||||||
|
file in a collection repository.
|
||||||
|
|
||||||
Implementations must never raise: enrichment treats a missing
|
Implementations must never raise: enrichment treats a missing
|
||||||
context as "the site had nothing extra to say".
|
context as "the site had nothing extra to say".
|
||||||
"""
|
"""
|
||||||
@@ -371,6 +399,7 @@ __all__ = [
|
|||||||
"HTTP_TIMEOUT",
|
"HTTP_TIMEOUT",
|
||||||
"ModelCardContext",
|
"ModelCardContext",
|
||||||
"ModelSource",
|
"ModelSource",
|
||||||
|
"ModelSourceCache",
|
||||||
"ModelSourceError",
|
"ModelSourceError",
|
||||||
"SourceRef",
|
"SourceRef",
|
||||||
"USER_AGENT",
|
"USER_AGENT",
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ which redirects to a CDN URL carrying a time-limited ``auth_key``.
|
|||||||
Requesting the resolve URL fresh on every attempt (which the shared
|
Requesting the resolve URL fresh on every attempt (which the shared
|
||||||
downloader does, including for resumable Range requests) keeps that key
|
downloader does, including for resumable Range requests) keeps that key
|
||||||
valid; the CDN URL must never be cached.
|
valid; the CDN URL must never be cached.
|
||||||
|
|
||||||
|
The README and the detail payload both describe the whole repository rather
|
||||||
|
than one file, so a per-run ``ModelSourceCache`` keeps them from being read
|
||||||
|
again for every checkpoint of a collection repository.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -32,7 +36,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
ModelCardContext,
|
ModelCardContext,
|
||||||
@@ -43,6 +47,9 @@ from .base import (
|
|||||||
filter_weight_files,
|
filter_weight_files,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
||||||
|
from .base import ModelSourceCache
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_URL_PATTERN = re.compile(
|
_URL_PATTERN = re.compile(
|
||||||
@@ -106,7 +113,11 @@ class ModelScopeSource(ModelSource):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def fetch_model_card_context(
|
async def fetch_model_card_context(
|
||||||
self, source_id: str, filename: str = ""
|
self,
|
||||||
|
source_id: str,
|
||||||
|
filename: str = "",
|
||||||
|
*,
|
||||||
|
cache: Optional["ModelSourceCache"] = None,
|
||||||
) -> ModelCardContext:
|
) -> ModelCardContext:
|
||||||
"""Read the model-detail API that backs the ModelScope model page.
|
"""Read the model-detail API that backs the ModelScope model page.
|
||||||
|
|
||||||
@@ -121,30 +132,44 @@ class ModelScopeSource(ModelSource):
|
|||||||
``stats.fileList``, which means the images returned belong to the
|
``stats.fileList``, which means the images returned belong to the
|
||||||
exact ``.safetensors`` being enriched — essential for collection
|
exact ``.safetensors`` being enriched — essential for collection
|
||||||
repositories, where every checkpoint has its own sample image.
|
repositories, where every checkpoint has its own sample image.
|
||||||
|
|
||||||
|
The detail payload describes the whole repository and is therefore
|
||||||
|
shared across every file in it, so it is read through *cache* when the
|
||||||
|
caller supplies one; only the per-file selection is redone.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
data = await self._fetch_detail(source_id, cache=cache)
|
||||||
|
if data is None:
|
||||||
|
return ModelCardContext()
|
||||||
|
return _build_card_context(data, filename)
|
||||||
|
|
||||||
|
async def _fetch_detail(
|
||||||
|
self,
|
||||||
|
source_id: str,
|
||||||
|
*,
|
||||||
|
cache: Optional["ModelSourceCache"] = None,
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
"""Fetch (or reuse) the model-detail payload for *source_id*."""
|
||||||
|
|
||||||
|
cache_key = (self.platform, "detail", source_id)
|
||||||
|
if cache is not None and cache_key in cache.provider:
|
||||||
|
return cache.provider[cache_key]
|
||||||
|
|
||||||
status, payload = await fetch_json(
|
status, payload = await fetch_json(
|
||||||
f"https://modelscope.cn/api/v1/models/{source_id}"
|
f"https://modelscope.cn/api/v1/models/{source_id}"
|
||||||
)
|
)
|
||||||
if status != 200 or not isinstance(payload, dict):
|
if status != 200 or not isinstance(payload, dict):
|
||||||
logger.debug("ModelScope detail API returned HTTP %s for %s", status, source_id)
|
logger.debug(
|
||||||
return ModelCardContext()
|
"ModelScope detail API returned HTTP %s for %s", status, source_id
|
||||||
|
)
|
||||||
|
return None
|
||||||
data = payload.get("Data")
|
data = payload.get("Data")
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return ModelCardContext()
|
return None
|
||||||
|
|
||||||
context = ModelCardContext(
|
if cache is not None:
|
||||||
description=_clean_text(data.get("Description")),
|
cache.provider[cache_key] = data
|
||||||
base_model=_first_string(data.get("BaseModel")),
|
return data
|
||||||
base_model_aliases=_base_model_aliases(data),
|
|
||||||
official_tags=_official_tags(data.get("OfficialTags")),
|
|
||||||
)
|
|
||||||
|
|
||||||
versions = _matching_versions(data.get("MuseInfo"), filename)
|
|
||||||
if versions:
|
|
||||||
context.example_images = _cover_image_urls(versions)
|
|
||||||
context.trigger_words = _version_trigger_words(versions)
|
|
||||||
return context
|
|
||||||
|
|
||||||
async def list_files(
|
async def list_files(
|
||||||
self, source_id: str, revision: str = ""
|
self, source_id: str, revision: str = ""
|
||||||
@@ -220,6 +245,28 @@ def _first_string(value: Any) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _build_card_context(data: dict[str, Any], filename: str) -> ModelCardContext:
|
||||||
|
"""Turn a model-detail payload into a :class:`ModelCardContext`.
|
||||||
|
|
||||||
|
Separated from the HTTP fetch so the repository-wide payload can be cached
|
||||||
|
across the files of a collection repository while the per-file selection
|
||||||
|
is still redone for each one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
context = ModelCardContext(
|
||||||
|
description=_clean_text(data.get("Description")),
|
||||||
|
base_model=_first_string(data.get("BaseModel")),
|
||||||
|
base_model_aliases=_base_model_aliases(data),
|
||||||
|
official_tags=_official_tags(data.get("OfficialTags")),
|
||||||
|
)
|
||||||
|
|
||||||
|
versions = _matching_versions(data.get("MuseInfo"), filename)
|
||||||
|
if versions:
|
||||||
|
context.example_images = _cover_image_urls(versions)
|
||||||
|
context.trigger_words = _version_trigger_words(versions)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def _base_model_aliases(data: dict[str, Any]) -> list[str]:
|
def _base_model_aliases(data: dict[str, Any]) -> list[str]:
|
||||||
"""Return the site's own names for the base model.
|
"""Return the site's own names for the base model.
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,10 @@ class TestBuildPromptContext:
|
|||||||
|
|
||||||
mock_fetch.assert_awaited_once_with("jj3550945163/Krea-2-LORA")
|
mock_fetch.assert_awaited_once_with("jj3550945163/Krea-2-LORA")
|
||||||
# The per-file lookup must receive the basename, not the full path.
|
# The per-file lookup must receive the basename, not the full path.
|
||||||
mock_context.assert_awaited_once_with(
|
mock_context.assert_awaited_once()
|
||||||
"jj3550945163/Krea-2-LORA", "krea.safetensors"
|
assert mock_context.call_args.args == (
|
||||||
|
"jj3550945163/Krea-2-LORA",
|
||||||
|
"krea.safetensors",
|
||||||
)
|
)
|
||||||
assert context["source_platform"] == "modelscope"
|
assert context["source_platform"] == "modelscope"
|
||||||
assert context["source_id"] == "jj3550945163/Krea-2-LORA"
|
assert context["source_id"] == "jj3550945163/Krea-2-LORA"
|
||||||
@@ -444,7 +446,8 @@ class TestLoadSourceCard:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_ctx.assert_awaited_once_with("u/r", "krea.safetensors")
|
mock_ctx.assert_awaited_once()
|
||||||
|
assert mock_ctx.call_args.args == ("u/r", "krea.safetensors")
|
||||||
assert context is card
|
assert context is card
|
||||||
assert variables["readme_content_full"] == "# card"
|
assert variables["readme_content_full"] == "# card"
|
||||||
assert variables["source_description"] == "作者说明"
|
assert variables["source_description"] == "作者说明"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from py.services.model_sources import (
|
from py.services.model_sources import (
|
||||||
|
ModelSourceCache,
|
||||||
HuggingFaceSource,
|
HuggingFaceSource,
|
||||||
ModelScopeSource,
|
ModelScopeSource,
|
||||||
TensorArtSource,
|
TensorArtSource,
|
||||||
@@ -688,3 +689,113 @@ class TestDownloadSourceRegistry:
|
|||||||
assert get_download_source("nope") is None
|
assert get_download_source("nope") is None
|
||||||
assert get_download_source("modelscope").platform == "modelscope"
|
assert get_download_source("modelscope").platform == "modelscope"
|
||||||
assert get_download_source("huggingface").platform == "huggingface"
|
assert get_download_source("huggingface").platform == "huggingface"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-run model-card cache
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelSourceCache:
|
||||||
|
def test_starts_empty(self):
|
||||||
|
cache = ModelSourceCache()
|
||||||
|
assert cache.readmes == {}
|
||||||
|
assert cache.provider == {}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedModelCardFetch:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_detail_payload_is_fetched_once_per_source_id(self, monkeypatch):
|
||||||
|
"""A collection repo's files share one detail request, not one each."""
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
async def fake_fetch_json(url, **_kwargs):
|
||||||
|
calls.append(url)
|
||||||
|
return 200, _modelscope_detail_payload()
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||||
|
)
|
||||||
|
|
||||||
|
source = ModelScopeSource()
|
||||||
|
cache = ModelSourceCache()
|
||||||
|
filenames = [
|
||||||
|
"Krea-2-LORA_c1-st1000.safetensors",
|
||||||
|
"Krea-2-LORA_c1-st8000.safetensors",
|
||||||
|
"Krea-2-LORA_c1-st1000.safetensors",
|
||||||
|
]
|
||||||
|
for name in filenames:
|
||||||
|
await source.fetch_model_card_context("u/r", name, cache=cache)
|
||||||
|
|
||||||
|
assert calls == ["https://modelscope.cn/api/v1/models/u/r"]
|
||||||
|
assert ("modelscope", "detail", "u/r") in cache.provider
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_per_file_selection_still_runs_for_each_file(self, monkeypatch):
|
||||||
|
"""The cached payload must not leak one file's images to another."""
|
||||||
|
|
||||||
|
async def fake_fetch_json(url, **_kwargs):
|
||||||
|
return 200, _modelscope_detail_payload()
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||||
|
)
|
||||||
|
|
||||||
|
source = ModelScopeSource()
|
||||||
|
cache = ModelSourceCache()
|
||||||
|
|
||||||
|
st1000 = await source.fetch_model_card_context(
|
||||||
|
"u/r", "Krea-2-LORA_c1-st1000.safetensors", cache=cache
|
||||||
|
)
|
||||||
|
st8000 = await source.fetch_model_card_context(
|
||||||
|
"u/r", "Krea-2-LORA_c1-st8000.safetensors", cache=cache
|
||||||
|
)
|
||||||
|
|
||||||
|
assert st1000.example_images == [
|
||||||
|
"https://resources.modelscope.cn/cover-images/b.png",
|
||||||
|
"https://resources.modelscope.cn/cover-images/c.png",
|
||||||
|
]
|
||||||
|
assert st8000.example_images == [
|
||||||
|
"https://resources.modelscope.cn/cover-images/a.png"
|
||||||
|
]
|
||||||
|
assert st8000.trigger_words == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failures_are_not_cached(self, monkeypatch):
|
||||||
|
"""A transient error must be retried for the next file."""
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
async def fake_fetch_json(url, **_kwargs):
|
||||||
|
calls.append(url)
|
||||||
|
return 500, None
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||||
|
)
|
||||||
|
|
||||||
|
source = ModelScopeSource()
|
||||||
|
cache = ModelSourceCache()
|
||||||
|
for _ in range(2):
|
||||||
|
context = await source.fetch_model_card_context("u/r", "a.safetensors", cache=cache)
|
||||||
|
assert context.is_empty()
|
||||||
|
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert cache.provider == {}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_no_cache_keeps_the_uncached_behaviour(self, monkeypatch):
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
async def fake_fetch_json(url, **_kwargs):
|
||||||
|
calls.append(url)
|
||||||
|
return 200, _modelscope_detail_payload()
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||||
|
)
|
||||||
|
|
||||||
|
source = ModelScopeSource()
|
||||||
|
for _ in range(2):
|
||||||
|
await source.fetch_model_card_context("u/r", "a.safetensors")
|
||||||
|
|
||||||
|
assert len(calls) == 2
|
||||||
|
|||||||
Reference in New Issue
Block a user