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
+31 -2
View File
@@ -25,7 +25,7 @@ import logging
import os
import re
from dataclasses import dataclass, field
from typing import Any, Iterable, Optional
from typing import Any, Dict, Iterable, Optional
import aiohttp
@@ -125,6 +125,26 @@ class ModelSourceError(Exception):
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
#: 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.
@@ -283,7 +303,11 @@ class ModelSource:
return ""
async def fetch_model_card_context(
self, source_id: str, filename: str = ""
self,
source_id: str,
filename: str = "",
*,
cache: Optional["ModelSourceCache"] = None,
) -> ModelCardContext:
"""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
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
context as "the site had nothing extra to say".
"""
@@ -371,6 +399,7 @@ __all__ = [
"HTTP_TIMEOUT",
"ModelCardContext",
"ModelSource",
"ModelSourceCache",
"ModelSourceError",
"SourceRef",
"USER_AGENT",