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:
Will Miao
2026-09-14 21:24:57 +08:00
parent f0ee30fc68
commit e9e9ee20c6
7 changed files with 247 additions and 26 deletions
+111
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import pytest
from py.services.model_sources import (
ModelSourceCache,
HuggingFaceSource,
ModelScopeSource,
TensorArtSource,
@@ -688,3 +689,113 @@ class TestDownloadSourceRegistry:
assert get_download_source("nope") is None
assert get_download_source("modelscope").platform == "modelscope"
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