Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
00579c1
bump version
wangxingjun778 Jul 20, 2026
31f224c
fix(download): forward progress_callbacks through HubApi.download_rep…
wangxingjun778 Jul 20, 2026
85436d9
merge main
wangxingjun778 Jul 20, 2026
306f145
fix(download): harden legacy cache auto-detection for pre-1.38 layouts
wangxingjun778 Jul 21, 2026
e4acbfe
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Jul 21, 2026
07aec55
fix(packaging): rename console scripts to modelscope-hub/ms-hub to av…
wangxingjun778 Jul 21, 2026
47b866a
update cli: ms/modelscope -> ms-hub/modelscope-hub
wangxingjun778 Jul 21, 2026
84b6e64
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Jul 21, 2026
dde1875
docs(readme): expand recent version news, fold older, group by type
wangxingjun778 Jul 21, 2026
d0ea9e6
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Jul 22, 2026
3665978
fix revision pass
wangxingjun778 Jul 22, 2026
a9839e6
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Jul 31, 2026
17244b2
bump version
wangxingjun778 Jul 31, 2026
91a5489
fix lint and NixOS UT
wangxingjun778 Jul 31, 2026
402f7c5
update readme
wangxingjun778 Jul 31, 2026
2375141
fix 3.10 citest
wangxingjun778 Jul 31, 2026
74a6357
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 1, 2026
2d082bd
fix(auth): stop misreporting login failures and revoking credentials
wangxingjun778 Aug 1, 2026
bb2eb63
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 1, 2026
ce5c7b8
update news and bump version
wangxingjun778 Aug 1, 2026
3facbce
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 3, 2026
d59c734
fix(api): recover full repo file list past the server's 3000-entry cap
wangxingjun778 Aug 4, 2026
8287606
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 6, 2026
89f2924
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 13, 2026
9ae5a5c
Move CLI script ownership to modelscope-hub
wangxingjun778 Aug 18, 2026
cc83c47
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 19, 2026
dfa1487
fix logout
wangxingjun778 Aug 24, 2026
836c3fe
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 24, 2026
2497d0f
fix lock file path
wangxingjun778 Aug 24, 2026
442b869
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 24, 2026
c7411e6
Add built-in studio CLI group
wangxingjun778 Aug 25, 2026
47df062
Harden credential file permissions
wangxingjun778 Aug 25, 2026
ade3430
Fix download compatibility and whoami parsing
wangxingjun778 Aug 25, 2026
3848d9f
ok Merge branch 'main' of github.com:modelscope/modelscope_hub into f…
wangxingjun778 Aug 27, 2026
e27ad71
fix legacy api
wangxingjun778 Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/modelscope_hub/_legacy_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ def create_repo(self, repo_type: str, body: dict[str, Any]) -> dict:
resp = self._request("POST", segment, json_body=body)
return self._json_data(resp)

def create_aigc_model(self, body: dict[str, Any]) -> dict:
"""POST /api/v1/models/aigc — create an AIGC model repository.

AIGC repositories use a dedicated legacy endpoint and payload shape;
routing them through :meth:`create_repo` would incorrectly target the
ordinary ``/models`` endpoint.
"""
resp = self._request("POST", "models/aigc", json_body=body)
return self._json_data(resp)

def get_repo_info(self, repo_id: str, repo_type: str) -> dict:
"""GET /api/v1/{type}s/{repo_id} — fetch repository metadata.

Expand Down Expand Up @@ -638,6 +648,11 @@ def list_revisions_detail(
)
return [], []

def create_aigc_model_tag(self, body: dict[str, Any]) -> dict:
"""POST /api/v1/models/aigc/repo/tag — create an AIGC model version."""
resp = self._request("POST", "models/aigc/repo/tag", json_body=body)
return self._json_data(resp)

def create_tag(
self,
repo_id: str,
Expand Down
145 changes: 142 additions & 3 deletions src/modelscope_hub/compat/hub_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from urllib.parse import urlencode

from ..api import HubApi
from ..constants import RepoType
from ..constants import RepoType, Visibility
from ..errors import (
AlreadyExistsError,
AuthenticationError,
Expand All @@ -35,6 +35,32 @@
META_FILES_FORMAT = {".json", ".csv", ".jsonl", ".tsv", ".py"}


class _AigcUploadAdapter:
"""Expose model-only upload methods expected by legacy ``AigcModel``."""

def __init__(self, api: HubApi) -> None:
self._api = api

def upload_file(self, *, repo_id: str, path_or_fileobj: Any, path_in_repo: str, **kwargs: Any) -> dict:
kwargs.pop("token", None)
return self._api.upload_file(
repo_id=repo_id,
repo_type=RepoType.MODEL,
path_or_fileobj=path_or_fileobj,
path_in_repo=path_in_repo,
**kwargs,
)

def upload_folder(self, *, repo_id: str, folder_path: Any, **kwargs: Any) -> dict | list[dict] | None:
kwargs.pop("token", None)
return self._api.upload_folder(
repo_id=repo_id,
repo_type=RepoType.MODEL,
folder_path=folder_path,
**kwargs,
)


class LegacyHubApi:
"""Drop-in replacement for the old ``modelscope.hub.api.HubApi``.

Expand Down Expand Up @@ -164,14 +190,17 @@ def create_repo(
def create_model(self, model_id: str, **kwargs: Any) -> str:
"""Create a model repo (legacy signature).

Returns the model repository URL for backward compatibility.
Converts authentication errors to ``ValueError`` for legacy callers.
AIGC models retain their dedicated endpoint and payload mapping. Plain
models continue to use the unified :meth:`create_repo` path.
"""
# Pre-normalize: convert numeric string to int for backward compatibility
visibility = kwargs.get("visibility")
if isinstance(visibility, str) and visibility.isdigit():
kwargs["visibility"] = int(visibility)
try:
aigc_model = kwargs.pop("aigc_model", None)
if aigc_model is not None:
return self._create_aigc_model(model_id, aigc_model, kwargs)
self.create_repo(model_id, repo_type="model", **kwargs)
except (AuthenticationError, InvalidParameter) as e:
if _is_auth_related(e):
Expand All @@ -180,6 +209,116 @@ def create_model(self, model_id: str, **kwargs: Any) -> str:
ep = self._endpoint or self._api._config.endpoint
return f"{ep}/models/{model_id}"

def _create_aigc_model(self, model_id: str, aigc_model: Any, kwargs: dict[str, Any]) -> str:
"""Create an AIGC model without changing the plain-model code path."""
token = kwargs.pop("token", None)
endpoint = kwargs.pop("endpoint", None)
visibility = kwargs.pop("visibility", None)
license_name = kwargs.pop("license", None)
chinese_name = kwargs.pop("chinese_name", None)
original_model_id = kwargs.pop("original_model_id", "")
gated_mode = kwargs.pop("gated_mode", None)
if kwargs:
unexpected = ", ".join(sorted(kwargs))
raise TypeError(f"create_model() got unexpected keyword argument(s): {unexpected}")

api = self._api
if token or endpoint:
api = HubApi(token=token, endpoint=endpoint or self._endpoint)
owner, name = api._parse_repo_id(model_id)
normalised_visibility = api._normalize_visibility(visibility)
if normalised_visibility is None:
normalised_visibility = int(Visibility.PUBLIC)

body: dict[str, Any] = {
"Path": owner,
"Name": name,
"ChineseName": chinese_name,
"Visibility": normalised_visibility,
"License": license_name or "Apache License 2.0",
"OriginalModelId": original_model_id,
"TrainId": os.environ.get("MODELSCOPE_TRAIN_ID", ""),
"TagShowName": aigc_model.tag,
"CoverImages": aigc_model.cover_images,
"AigcType": aigc_model.aigc_type,
"TagDescription": aigc_model.description,
"VisionFoundation": aigc_model.base_model_type,
"BaseModel": aigc_model.base_model_id or original_model_id,
"WeightsName": aigc_model.weight_filename,
"WeightsSha256": aigc_model.weight_sha256,
"WeightsSize": aigc_model.weight_size,
"ModelPath": aigc_model.model_path,
"TriggerWords": aigc_model.trigger_words,
"ModelSource": aigc_model.model_source,
"SubVisionFoundation": aigc_model.base_model_sub_type,
}
if aigc_model.official_tags:
body["OfficialTags"] = aigc_model.official_tags
if gated_mode is not None:
if normalised_visibility == int(Visibility.PRIVATE):
body["ProtectedMode"] = 1 if gated_mode else 2
else:
logger.warning("gated_mode is only effective when visibility is PRIVATE, ignored.")

cookies = api.get_cookies(access_token=token, cookies_required=True)
aigc_model.preupload_weights(
cookies=cookies,
headers={},
endpoint=api._config.endpoint,
)
api.legacy.create_aigc_model(body)
aigc_model.upload_to_repo(_AigcUploadAdapter(api), model_id, token)
return f"{api._config.endpoint}/models/{model_id}"

def create_model_tag(
self,
model_id: str,
tag_name: str,
endpoint: str | None = None,
token: str | None = None,
aigc_model: Any = None,
) -> str:
"""Create a model tag while preserving the AIGC-specific endpoint."""
if not model_id:
raise InvalidParameter("model_id is required!")
if not tag_name:
raise InvalidParameter("tag_name is required!")
if tag_name.lower() in {"main", "master"}:
raise InvalidParameter(
f'tag_name "{tag_name}" is not allowed. '
'Please use a different tag name (e.g., "v1.0", "v1.1", "latest"). '
'Reserved names: main, master'
)

api = self._api
if token or endpoint:
api = HubApi(token=token, endpoint=endpoint or self._endpoint)
if aigc_model is None:
api.create_repo_tag(model_id, RepoType.MODEL, tag_name, revision="master")
else:
owner, name = api._parse_repo_id(model_id)
cookies = api.get_cookies(access_token=token, cookies_required=True)
aigc_model.preupload_weights(
cookies=cookies,
headers={},
endpoint=api._config.endpoint,
)
api.legacy.create_aigc_model_tag(
{
"CoverImages": aigc_model.cover_images,
"Name": name,
"Path": owner,
"TagShowName": tag_name,
"WeightsName": aigc_model.weight_filename,
"WeightsSha256": aigc_model.weight_sha256,
"WeightsSize": aigc_model.weight_size,
"TriggerWords": aigc_model.trigger_words,
"AigcType": aigc_model.aigc_type,
"VisionFoundation": aigc_model.base_model_type,
}
)
return f"{api._config.endpoint}/models/{model_id}/tags/{tag_name}"

def push_model(self, model_id: str, model_dir: str, **kwargs: Any) -> None:
"""Upload a model directory (legacy signature)."""
# Pre-validate model_dir
Expand Down
157 changes: 152 additions & 5 deletions tests/test_compat_get_model_files.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Unit tests for the legacy-compatible ``LegacyHubApi.get_model_files``.
"""Unit tests for selected legacy-compatible ``LegacyHubApi`` methods.

Network-free: the underlying ``HubApi.list_repo_files`` is mocked so we only
verify the compat wrapper's signature and parameter forwarding. Regression
guard for callers (e.g. vLLM) that pass the historical ``revision`` / ``root``
keyword arguments.
Network-free: the bottom-level HTTP transport is mocked where full compat
call-chain coverage matters.
"""

from __future__ import annotations
Expand All @@ -14,6 +12,33 @@
from modelscope_hub.compat import LegacyHubApi


class _FakeAigcModel:
tag = "v1.0"
cover_images = ["data:image/png;base64,AAAA"]
aigc_type = "LoRA"
description = "AIGC compatibility test"
base_model_type = "SD_XL"
base_model_id = "owner/base-model"
weight_filename = "model.safetensors"
weight_sha256 = "abc123"
weight_size = 42
model_path = "/tmp/model.safetensors"
trigger_words = ["trigger"]
model_source = "USER_UPLOAD"
base_model_sub_type = "SD_XL"
official_tags = ["photography"]

def __init__(self):
self.preupload_weights = mock.MagicMock()
self.upload_to_repo = mock.MagicMock(return_value=True)


def _response(data=None):
response = mock.MagicMock()
response.json.return_value = {"Data": data or {}}
return response


def _fake_files():
return [
SimpleNamespace(path="config.json", size=10),
Expand Down Expand Up @@ -59,3 +84,125 @@ def test_default_revision_none_forwarded(self):
_, kwargs = m.call_args
assert kwargs["revision"] is None
assert kwargs["recursive"] is True


class TestCreateModelLegacyCompat:
def test_aigc_model_uses_dedicated_endpoint_and_payload(self):
api = LegacyHubApi(endpoint="https://modelscope.cn", token="ms-test")
aigc_model = _FakeAigcModel()

with mock.patch.object(api._api.legacy, "_request", return_value=_response()) as request:
url = api.create_model(
"owner/aigc-model",
visibility=1,
license="Apache License 2.0",
chinese_name="AIGC 模型",
original_model_id="owner/original-model",
aigc_model=aigc_model,
gated_mode=False,
)

request.assert_called_once()
method, path = request.call_args.args
body = request.call_args.kwargs["json_body"]
assert (method, path) == ("POST", "models/aigc")
assert body == {
"Path": "owner",
"Name": "aigc-model",
"ChineseName": "AIGC 模型",
"Visibility": 1,
"License": "Apache License 2.0",
"OriginalModelId": "owner/original-model",
"TrainId": "",
"TagShowName": "v1.0",
"CoverImages": ["data:image/png;base64,AAAA"],
"AigcType": "LoRA",
"TagDescription": "AIGC compatibility test",
"VisionFoundation": "SD_XL",
"BaseModel": "owner/base-model",
"WeightsName": "model.safetensors",
"WeightsSha256": "abc123",
"WeightsSize": 42,
"ModelPath": "/tmp/model.safetensors",
"TriggerWords": ["trigger"],
"ModelSource": "USER_UPLOAD",
"SubVisionFoundation": "SD_XL",
"OfficialTags": ["photography"],
"ProtectedMode": 2,
}
assert url == "https://modelscope.cn/models/owner/aigc-model"
aigc_model.preupload_weights.assert_called_once()
preupload_kwargs = aigc_model.preupload_weights.call_args.kwargs
assert preupload_kwargs["cookies"]["m_session_id"] == "ms-test"
assert preupload_kwargs["endpoint"] == "https://modelscope.cn"
aigc_model.upload_to_repo.assert_called_once()
upload_api, model_id, token = aigc_model.upload_to_repo.call_args.args
assert upload_api._api is api._api
assert model_id == "owner/aigc-model"
assert token is None

def test_plain_model_keeps_generic_create_repo_path(self):
api = LegacyHubApi(endpoint="https://modelscope.cn", token="ms-test")

with mock.patch.object(api._api.legacy, "_request", return_value=_response()) as request:
url = api.create_model(
"owner/plain-model",
visibility=1,
license="Apache License 2.0",
chinese_name="普通模型",
aigc_model=None,
)

request.assert_called_once()
method, path = request.call_args.args
body = request.call_args.kwargs["json_body"]
assert (method, path) == ("POST", "models")
assert body["Path"] == "owner"
assert body["Name"] == "plain-model"
assert "TagShowName" not in body
assert "aigc_model" not in body
assert url == "https://modelscope.cn/models/owner/plain-model"

def test_aigc_model_tag_uses_dedicated_endpoint(self):
api = LegacyHubApi(endpoint="https://modelscope.cn", token="ms-test")
aigc_model = _FakeAigcModel()

with mock.patch.object(api._api.legacy, "_request", return_value=_response()) as request:
url = api.create_model_tag(
"owner/aigc-model",
"v1.1",
aigc_model=aigc_model,
)

request.assert_called_once()
method, path = request.call_args.args
assert (method, path) == ("POST", "models/aigc/repo/tag")
assert request.call_args.kwargs["json_body"] == {
"CoverImages": ["data:image/png;base64,AAAA"],
"Name": "aigc-model",
"Path": "owner",
"TagShowName": "v1.1",
"WeightsName": "model.safetensors",
"WeightsSha256": "abc123",
"WeightsSize": 42,
"TriggerWords": ["trigger"],
"AigcType": "LoRA",
"VisionFoundation": "SD_XL",
}
aigc_model.preupload_weights.assert_called_once()
assert url == "https://modelscope.cn/models/owner/aigc-model/tags/v1.1"

def test_plain_model_tag_keeps_generic_endpoint(self):
api = LegacyHubApi(endpoint="https://modelscope.cn", token="ms-test")

with mock.patch.object(api._api.legacy, "_request", return_value=_response()) as request:
url = api.create_model_tag("owner/plain-model", "v1.1")

request.assert_called_once()
method, path = request.call_args.args
assert (method, path) == ("POST", "models/owner/plain-model/repo/tag")
assert request.call_args.kwargs["json_body"] == {
"TagName": "v1.1",
"Ref": "master",
}
assert url == "https://modelscope.cn/models/owner/plain-model/tags/v1.1"
Loading