From 60ba9e9a8b32fb34cb764d221e2f941409b55b45 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 8 Sep 2026 13:04:20 -0700 Subject: [PATCH 1/2] feat(models): support boolean fileset filter on ModelEntityFilter fileset was Optional[str], the only filter on the field limited to an exact fileset reference. base_model already supports Optional[Union[..., bool, str]], letting callers ask true/false for presence via the generic bool-coercible-field machinery in Filter._get_bool_coercible_fields(). Widen fileset to Optional[Union[bool, str]] the same way so a caller can filter for models that have (or don't have) a fileset without walking every page client-side to find one. Follow-up to ASTD-562/ASTD-567: the Base Model dropdown filters for fine-tunable models (fileset must be set) entirely client-side today, which forces it to page past runs of ineligible models one round trip at a time. This unblocks moving that filter server-side. Signed-off-by: mschwab --- openapi/ga/individual/platform.openapi.yaml | 7 +++- openapi/ga/openapi.yaml | 7 +++- openapi/openapi.yaml | 7 +++- .../models/src/nmp/core/models/schemas.py | 5 ++- .../models/tests/unit/api/test_models_api.py | 40 +++++++++++++++++++ 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 3231068474..fbd7c44f42 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -15566,9 +15566,12 @@ components: description: Filter by description. title: Description fileset: - description: Filter by fileset reference in the form {workspace}/{fileset_name}. + anyOf: + - type: boolean + - type: string + description: 'Filter by fileset: true = has a fileset, false = no fileset, + string = match fileset reference in the form {workspace}/{fileset_name}.' title: Fileset - type: string created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 3231068474..fbd7c44f42 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -15566,9 +15566,12 @@ components: description: Filter by description. title: Description fileset: - description: Filter by fileset reference in the form {workspace}/{fileset_name}. + anyOf: + - type: boolean + - type: string + description: 'Filter by fileset: true = has a fileset, false = no fileset, + string = match fileset reference in the form {workspace}/{fileset_name}.' title: Fileset - type: string created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 3231068474..fbd7c44f42 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -15566,9 +15566,12 @@ components: description: Filter by description. title: Description fileset: - description: Filter by fileset reference in the form {workspace}/{fileset_name}. + anyOf: + - type: boolean + - type: string + description: 'Filter by fileset: true = has a fileset, false = no fileset, + string = match fileset reference in the form {workspace}/{fileset_name}.' title: Fileset - type: string created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/services/core/models/src/nmp/core/models/schemas.py b/services/core/models/src/nmp/core/models/schemas.py index 722d3e5ebd..ac7471b73c 100644 --- a/services/core/models/src/nmp/core/models/schemas.py +++ b/services/core/models/src/nmp/core/models/schemas.py @@ -1278,9 +1278,10 @@ class ModelEntityFilter(Filter): description="Filter models by whether their deployment config has LoRA enabled.", ) description: StringFilter | str | None = Field(None, description="Filter by description.") - fileset: Optional[str] = Field( + fileset: Optional[Union[bool, str]] = Field( None, - description="Filter by fileset reference in the form {workspace}/{fileset_name}.", + description="Filter by fileset: true = has a fileset, false = no fileset, " + "string = match fileset reference in the form {workspace}/{fileset_name}.", ) created_at: Optional[DatetimeFilter] = Field(None, description="Filter entities based on creation date.") updated_at: Optional[DatetimeFilter] = Field(None, description="Filter entities based on update date.") diff --git a/services/core/models/tests/unit/api/test_models_api.py b/services/core/models/tests/unit/api/test_models_api.py index 83997c3ced..38a8dfcf66 100644 --- a/services/core/models/tests/unit/api/test_models_api.py +++ b/services/core/models/tests/unit/api/test_models_api.py @@ -252,6 +252,46 @@ def test_list_models_with_base_model_filter_name(client, mock_model_entity_servi assert parsed_filter.extract("data.base_model") == "llama-3" +def test_list_models_with_fileset_filter_ref(client, mock_model_entity_service, sample_page): + """Test listing models filtered by fileset reference using $eq.""" + mock_model_entity_service.list_model_entities.return_value = sample_page + + response = client.get("/apis/models/v2/workspaces/nvidia/models?filter[fileset]=nvidia/my-fileset") + + assert response.status_code == 200 + call_args = mock_model_entity_service.list_model_entities.call_args + parsed_filter = call_args.kwargs["parsed_filter"] + assert parsed_filter.extract("data.fileset") == "nvidia/my-fileset" + + +def test_list_models_with_fileset_filter_true(client, mock_model_entity_service, sample_page): + """Test listing models filtered by fileset=true → $not { data.fileset $eq null }.""" + mock_model_entity_service.list_model_entities.return_value = sample_page + + response = client.get("/apis/models/v2/workspaces/nvidia/models?filter[fileset]=true") + + assert response.status_code == 200 + call_args = mock_model_entity_service.list_model_entities.call_args + parsed_filter = call_args.kwargs["parsed_filter"] + assert parsed_filter.operation is not None + # Bool "true" is coerced to a not-null check + assert parsed_filter.operation.to_dict() == {"$not": {"data.fileset": {"$eq": None}}} + + +def test_list_models_with_fileset_filter_false(client, mock_model_entity_service, sample_page): + """Test listing models filtered by fileset=false → data.fileset $eq null.""" + mock_model_entity_service.list_model_entities.return_value = sample_page + + response = client.get("/apis/models/v2/workspaces/nvidia/models?filter[fileset]=false") + + assert response.status_code == 200 + call_args = mock_model_entity_service.list_model_entities.call_args + parsed_filter = call_args.kwargs["parsed_filter"] + assert parsed_filter.operation is not None + # Bool "false" is coerced to a null check + assert parsed_filter.operation.to_dict() == {"data.fileset": {"$eq": None}} + + def test_list_models_with_name_filter(client, mock_model_entity_service, sample_page): """Test listing models with name filter using $like operator.""" mock_model_entity_service.list_model_entities.return_value = sample_page From 807680c98a708591c0e72b58c4f15faf32807a23 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 8 Sep 2026 15:09:55 -0700 Subject: [PATCH 2/2] chore(sdk): manually patch vendored Python SDK for fileset bool filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stainless's cloud build queue has been stuck (builds stalled at not_started for hours, going back over a week of prior build errors across the project) — 'make stainless' cannot complete right now. This hand-patches the vendored SDK to match the fileset: Optional[Union[bool, str]] widening from 60ba9e9a8b, mirroring the existing base_model pattern, purely so 'nemo-platform-sdk-tools is-up-to-date' (and CI's lint-python-sdk) passes. This is a stopgap, not a real generation. Once Stainless recovers, run 'make stainless' for real — it may reformat these files differently (docstring wrapping, etc.) than this manual edit did. Signed-off-by: mschwab --- sdk/python/nemo-platform/.nmpcontext/openapi.yaml | 7 +++++-- .../types/models/model_entity_filter_param.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 699c1c7f77..78908c22f7 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -15569,9 +15569,12 @@ components: description: Filter by description. title: Description fileset: - description: Filter by fileset reference in the form {workspace}/{fileset_name}. + anyOf: + - type: boolean + - type: string + description: 'Filter by fileset: true = has a fileset, false = no fileset, + string = match fileset reference in the form {workspace}/{fileset_name}.' title: Fileset - type: string created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/models/model_entity_filter_param.py b/sdk/python/nemo-platform/src/nemo_platform/types/models/model_entity_filter_param.py index 796f697ef5..a232d96957 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/models/model_entity_filter_param.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/models/model_entity_filter_param.py @@ -26,7 +26,7 @@ from .finetuning_type_filter_param import FinetuningTypeFilterParam from ..shared_params.datetime_filter import DatetimeFilter -__all__ = ["ModelEntityFilterParam", "Adapters", "BaseModel", "Description", "Name"] +__all__ = ["ModelEntityFilterParam", "Adapters", "BaseModel", "Description", "Fileset", "Name"] Adapters: TypeAlias = Union[FinetuningTypeFilterParam, bool] @@ -34,6 +34,8 @@ Description: TypeAlias = Union[StringFilter, str] +Fileset: TypeAlias = Union[bool, str] + Name: TypeAlias = Union[StringFilter, str] @@ -55,8 +57,11 @@ class ModelEntityFilterParam(TypedDict, total=False): description: Description """Filter by description.""" - fileset: str - """Filter by fileset reference in the form {workspace}/{fileset_name}.""" + fileset: Fileset + """ + Filter by fileset: true = has a fileset, false = no fileset, string = match + fileset reference in the form {workspace}/{fileset_name}. + """ finetuning_type: Union[FinetuningType, bool] """Filter models that have been perviously finetuned."""