From 1b0d92c0ea4acf531203ed1e8d813de42955532a Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 14 Jul 2026 13:52:45 -0700 Subject: [PATCH 1/5] feat(studio): add Virtual Models view/delete page Add a Studio page to view and delete VirtualModels: list with a read-only details side panel (core fields + middleware pipelines) and delete, wired to the IGW virtual-models API and generated SDK. Route + sidebar nav under the Models group. Controller-managed passthrough VirtualModels are noise in the UI, so the IGW list endpoint gains an include_autoprovisioned flag (default true to preserve CLI/SDK behavior) plus a VirtualModelFilter for name/entity/date filtering. The Studio list passes include_autoprovisioned=false to hide them. Regenerated OpenAPI, Python SDK, and CLI accordingly. Signed-off-by: mschwab --- docs/cli/reference.mdx | 15 + openapi/ga/individual/platform.openapi.yaml | 58 ++++ openapi/ga/openapi.yaml | 58 ++++ openapi/openapi.yaml | 58 ++++ .../commands/api/inference/virtual_models.py | 38 ++- .../nemo-platform/.nmpcontext/openapi.yaml | 58 ++++ .../nemo-platform/.nmpcontext/stainless.yaml | 1 + sdk/python/nemo-platform/scripts/lint | 2 +- .../commands/api/inference/virtual_models.py | 38 ++- .../nemo_platform/resources/inference/api.md | 1 + .../resources/inference/virtual_models.py | 27 +- .../nemo_platform/types/inference/__init__.py | 1 + .../inference/virtual_model_filter_param.py | 55 ++++ .../inference/virtual_model_list_params.py | 14 + .../inference/test_virtual_models.py | 53 ++++ sdk/stainless.yaml | 1 + .../api/v2/virtual_models.py | 42 ++- .../tests/unit/test_virtual_models_router.py | 19 ++ .../VirtualModelsDataView/index.test.tsx | 112 ++++++++ .../dataViews/VirtualModelsDataView/index.tsx | 265 ++++++++++++++++++ web/packages/studio/src/constants/routes.ts | 1 + .../index.test.tsx | 47 ++++ .../VirtualModelDetailsSidePanel/index.tsx | 145 ++++++++++ .../routes/VirtualModelsListRoute/index.tsx | 43 +++ .../WorkspaceLayout/WorkspaceSideNav.tsx | 68 +++-- .../studio/src/routes/groups/index.ts | 1 + .../src/routes/groups/virtualModelsRoutes.tsx | 21 ++ web/packages/studio/src/routes/index.tsx | 2 + web/packages/studio/src/routes/utils.ts | 4 + 29 files changed, 1212 insertions(+), 36 deletions(-) create mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py create mode 100644 web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx create mode 100644 web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx create mode 100644 web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.test.tsx create mode 100644 web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx create mode 100644 web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx create mode 100644 web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index c1c41a50e6..1dc19bca55 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -3464,11 +3464,26 @@ nemo inference virtual-models list [OPTIONS] **Options:** * `--workspace` +* `--include-autoprovisioned`: When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. * `--page `: Page number (1-indexed). * `--page-size `: Number of results per page. * `--sort`: Sort field. Prefix with `-` for descending order. * `--all-pages`: Fetch all pages +**Filter Options:** + +* `--filter FILTER_JSON`: Use --filter with JSON for complex/nested queries, or --filter. FIELD options for simple fields. Both can be combined, with field options taking precedence. +JSON-only fields: + created_at: \{gte: str, lte: str} + updated_at: \{gte: str, lte: str} + +Filter virtual models by workspace, project, name, default_model_entity, autoprovisioned, created_at, and updated_at. +* `--filter.autoprovisioned` +* `--filter.default-model-entity` +* `--filter.name` +* `--filter.project` +* `--filter.workspace` + **Help:** * `--help, -h`: Show this message and exit. diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 4d5985d72a..0ddc455245 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -3024,6 +3024,26 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. + - name: include_autoprovisioned + in: query + required: false + schema: + type: boolean + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + default: true + title: Include Autoprovisioned + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/VirtualModelFilter' + description: Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18640,6 +18660,44 @@ components: empty middleware lists, ``default_model_entity`` pointing to that entity). All existing inference requests continue to work without changes.' + VirtualModelFilter: + additionalProperties: false + description: Filter for VirtualModel list queries. + properties: + workspace: + description: Filter by workspace. + title: Workspace + type: string + project: + description: Filter by project URN. + title: Project + type: string + name: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by name. + title: Name + default_model_entity: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by default model entity. + title: Default Model Entity + autoprovisioned: + description: Filter by whether the VirtualModel is controller-managed. + title: Autoprovisioned + type: boolean + created_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by creation date. + updated_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by update date. + title: VirtualModelFilter + type: object VirtualModelInferenceConfig: properties: model: diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 4d5985d72a..0ddc455245 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -3024,6 +3024,26 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. + - name: include_autoprovisioned + in: query + required: false + schema: + type: boolean + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + default: true + title: Include Autoprovisioned + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/VirtualModelFilter' + description: Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18640,6 +18660,44 @@ components: empty middleware lists, ``default_model_entity`` pointing to that entity). All existing inference requests continue to work without changes.' + VirtualModelFilter: + additionalProperties: false + description: Filter for VirtualModel list queries. + properties: + workspace: + description: Filter by workspace. + title: Workspace + type: string + project: + description: Filter by project URN. + title: Project + type: string + name: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by name. + title: Name + default_model_entity: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by default model entity. + title: Default Model Entity + autoprovisioned: + description: Filter by whether the VirtualModel is controller-managed. + title: Autoprovisioned + type: boolean + created_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by creation date. + updated_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by update date. + title: VirtualModelFilter + type: object VirtualModelInferenceConfig: properties: model: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 4d5985d72a..0ddc455245 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -3024,6 +3024,26 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. + - name: include_autoprovisioned + in: query + required: false + schema: + type: boolean + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + default: true + title: Include Autoprovisioned + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/VirtualModelFilter' + description: Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18640,6 +18660,44 @@ components: empty middleware lists, ``default_model_entity`` pointing to that entity). All existing inference requests continue to work without changes.' + VirtualModelFilter: + additionalProperties: false + description: Filter for VirtualModel list queries. + properties: + workspace: + description: Filter by workspace. + title: Workspace + type: string + project: + description: Filter by project URN. + title: Project + type: string + name: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by name. + title: Name + default_model_entity: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by default model entity. + title: Default Model Entity + autoprovisioned: + description: Filter by whether the VirtualModel is controller-managed. + title: Autoprovisioned + type: boolean + created_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by creation date. + updated_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by update date. + title: VirtualModelFilter + type: object VirtualModelInferenceConfig: properties: model: diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py index 37c2714e14..22ce93d794 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py @@ -8,7 +8,7 @@ import typer -from nemo_platform_ext.cli.core.api import build_kwargs +from nemo_platform_ext.cli.core.api import build_kwargs, merge_filter_dict from nemo_platform_ext.cli.core.code_generator import handle_code_generation from nemo_platform_ext.cli.core.context import CLIContext from nemo_platform_ext.cli.core.errors import handle_errors @@ -199,6 +199,33 @@ def delete_virtual_models( def list_virtual_models( ctx: typer.Context, workspace: Annotated[str | None, typer.Option("--workspace")] = None, + filter: Annotated[ + str | None, + typer.Option( + "--filter", + metavar="FILTER_JSON", + help="Use --filter with JSON for complex/nested queries, or --filter.FIELD options for simple fields. Both can be combined, with field options taking precedence.\nJSON-only fields:\n created_at: {gte: str, lte: str}\n updated_at: {gte: str, lte: str}\n\nFilter virtual models by workspace, project, name, default_model_entity, autoprovisioned, created_at, and updated_at.", + rich_help_panel="Filter Options", + ), + ] = None, + filter_autoprovisioned: Annotated[ + bool | None, typer.Option("--filter.autoprovisioned", rich_help_panel="Filter Options") + ] = None, + filter_default_model_entity: Annotated[ + str | None, typer.Option("--filter.default-model-entity", rich_help_panel="Filter Options") + ] = None, + filter_name: Annotated[str | None, typer.Option("--filter.name", rich_help_panel="Filter Options")] = None, + filter_project: Annotated[str | None, typer.Option("--filter.project", rich_help_panel="Filter Options")] = None, + filter_workspace: Annotated[ + str | None, typer.Option("--filter.workspace", rich_help_panel="Filter Options") + ] = None, + include_autoprovisioned: Annotated[ + bool | None, + typer.Option( + "--include-autoprovisioned", + help="When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results.", + ), + ] = None, page: Annotated[int | None, typer.Option("--page", help="Page number (1-indexed).")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Number of results per page.")] = None, sort: Annotated[ @@ -227,6 +254,15 @@ def list_virtual_models( kwargs = build_kwargs( workspace=workspace, + filter=merge_filter_dict( + filter, + autoprovisioned=filter_autoprovisioned, + default_model_entity=filter_default_model_entity, + name=filter_name, + project=filter_project, + workspace=filter_workspace, + ), + include_autoprovisioned=include_autoprovisioned, page=page, page_size=page_size, sort=sort, diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 4d5985d72a..0ddc455245 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -3024,6 +3024,26 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. + - name: include_autoprovisioned + in: query + required: false + schema: + type: boolean + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + default: true + title: Include Autoprovisioned + description: When false, controller-managed (autoprovisioned) passthrough + VirtualModels are excluded from the results. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/VirtualModelFilter' + description: Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18640,6 +18660,44 @@ components: empty middleware lists, ``default_model_entity`` pointing to that entity). All existing inference requests continue to work without changes.' + VirtualModelFilter: + additionalProperties: false + description: Filter for VirtualModel list queries. + properties: + workspace: + description: Filter by workspace. + title: Workspace + type: string + project: + description: Filter by project URN. + title: Project + type: string + name: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by name. + title: Name + default_model_entity: + anyOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + description: Filter by default model entity. + title: Default Model Entity + autoprovisioned: + description: Filter by whether the VirtualModel is controller-managed. + title: Autoprovisioned + type: boolean + created_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by creation date. + updated_at: + allOf: + - $ref: '#/components/schemas/DatetimeFilter' + description: Filter by update date. + title: VirtualModelFilter + type: object VirtualModelInferenceConfig: properties: model: diff --git a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml index 4fbdee99d3..4172d676e3 100644 --- a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml @@ -385,6 +385,7 @@ resources: middleware_call: MiddlewareCall update_virtual_model_request: UpdateVirtualModelRequest virtual_model: VirtualModel + virtual_model_filter: VirtualModelFilter virtual_model_inference_config: VirtualModelInferenceConfig virtual_models_page: VirtualModelsPage methods: diff --git a/sdk/python/nemo-platform/scripts/lint b/sdk/python/nemo-platform/scripts/lint index 59e9166ad0..96ba935a93 100755 --- a/sdk/python/nemo-platform/scripts/lint +++ b/sdk/python/nemo-platform/scripts/lint @@ -13,7 +13,7 @@ else fi echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py index cf35492190..87c04edb04 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py @@ -8,7 +8,7 @@ import typer -from nemo_platform.cli.core.api import build_kwargs +from nemo_platform.cli.core.api import build_kwargs, merge_filter_dict from nemo_platform.cli.core.code_generator import handle_code_generation from nemo_platform.cli.core.context import CLIContext from nemo_platform.cli.core.errors import handle_errors @@ -199,6 +199,33 @@ def delete_virtual_models( def list_virtual_models( ctx: typer.Context, workspace: Annotated[str | None, typer.Option("--workspace")] = None, + filter: Annotated[ + str | None, + typer.Option( + "--filter", + metavar="FILTER_JSON", + help="Use --filter with JSON for complex/nested queries, or --filter.FIELD options for simple fields. Both can be combined, with field options taking precedence.\nJSON-only fields:\n created_at: {gte: str, lte: str}\n updated_at: {gte: str, lte: str}\n\nFilter virtual models by workspace, project, name, default_model_entity, autoprovisioned, created_at, and updated_at.", + rich_help_panel="Filter Options", + ), + ] = None, + filter_autoprovisioned: Annotated[ + bool | None, typer.Option("--filter.autoprovisioned", rich_help_panel="Filter Options") + ] = None, + filter_default_model_entity: Annotated[ + str | None, typer.Option("--filter.default-model-entity", rich_help_panel="Filter Options") + ] = None, + filter_name: Annotated[str | None, typer.Option("--filter.name", rich_help_panel="Filter Options")] = None, + filter_project: Annotated[str | None, typer.Option("--filter.project", rich_help_panel="Filter Options")] = None, + filter_workspace: Annotated[ + str | None, typer.Option("--filter.workspace", rich_help_panel="Filter Options") + ] = None, + include_autoprovisioned: Annotated[ + bool | None, + typer.Option( + "--include-autoprovisioned", + help="When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results.", + ), + ] = None, page: Annotated[int | None, typer.Option("--page", help="Page number (1-indexed).")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Number of results per page.")] = None, sort: Annotated[ @@ -227,6 +254,15 @@ def list_virtual_models( kwargs = build_kwargs( workspace=workspace, + filter=merge_filter_dict( + filter, + autoprovisioned=filter_autoprovisioned, + default_model_entity=filter_default_model_entity, + name=filter_name, + project=filter_project, + workspace=filter_workspace, + ), + include_autoprovisioned=include_autoprovisioned, page=page, page_size=page_size, sort=sort, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/inference/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/inference/api.md index dca686fe43..394f32b395 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/inference/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/inference/api.md @@ -10,6 +10,7 @@ from nemo_platform.types.inference import ( MiddlewareCall, UpdateVirtualModelRequest, VirtualModel, + VirtualModelFilter, VirtualModelInferenceConfig, VirtualModelsPage, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py b/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py index 5c0d331a2b..0e42a4d80f 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py @@ -33,9 +33,14 @@ ) from ...pagination import SyncDefaultPagination, AsyncDefaultPagination from ..._base_client import AsyncPaginator, make_request_options -from ...types.inference import virtual_model_list_params, virtual_model_patch_params, virtual_model_create_params +from ...types.inference import ( + virtual_model_list_params, + virtual_model_patch_params, + virtual_model_create_params, +) from ...types.inference.virtual_model import VirtualModel from ...types.inference.middleware_call_param import MiddlewareCallParam +from ...types.inference.virtual_model_filter_param import VirtualModelFilterParam from ...types.inference.virtual_model_inference_config_param import VirtualModelInferenceConfigParam from ..._exceptions import ConflictError @@ -208,6 +213,8 @@ def list( self, *, workspace: str | None = None, + filter: VirtualModelFilterParam | Omit = omit, + include_autoprovisioned: bool | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, sort: str | Omit = omit, @@ -224,6 +231,12 @@ def list( Use `workspace=-` to list across all workspaces accessible to the caller. Args: + filter: Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. + + include_autoprovisioned: When false, controller-managed (autoprovisioned) passthrough VirtualModels are + excluded from the results. + page: Page number (1-indexed). page_size: Number of results per page. @@ -252,6 +265,8 @@ def list( timeout=timeout, query=maybe_transform( { + "filter": filter, + "include_autoprovisioned": include_autoprovisioned, "page": page, "page_size": page_size, "sort": sort, @@ -568,6 +583,8 @@ def list( self, *, workspace: str | None = None, + filter: VirtualModelFilterParam | Omit = omit, + include_autoprovisioned: bool | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, sort: str | Omit = omit, @@ -584,6 +601,12 @@ def list( Use `workspace=-` to list across all workspaces accessible to the caller. Args: + filter: Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. + + include_autoprovisioned: When false, controller-managed (autoprovisioned) passthrough VirtualModels are + excluded from the results. + page: Page number (1-indexed). page_size: Number of results per page. @@ -612,6 +635,8 @@ def list( timeout=timeout, query=maybe_transform( { + "filter": filter, + "include_autoprovisioned": include_autoprovisioned, "page": page, "page_size": page_size, "sort": sort, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/inference/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/inference/__init__.py index 20356db654..2e2b34faaa 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/inference/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/inference/__init__.py @@ -56,6 +56,7 @@ from .virtual_model_list_params import VirtualModelListParams as VirtualModelListParams from .chat_completion_tool_param import ChatCompletionToolParam as ChatCompletionToolParam from .served_model_mapping_param import ServedModelMappingParam as ServedModelMappingParam +from .virtual_model_filter_param import VirtualModelFilterParam as VirtualModelFilterParam from .virtual_model_patch_params import VirtualModelPatchParams as VirtualModelPatchParams from .model_provider_filter_param import ModelProviderFilterParam as ModelProviderFilterParam from .virtual_model_create_params import VirtualModelCreateParams as VirtualModelCreateParams diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py new file mode 100644 index 0000000000..eacc631a04 --- /dev/null +++ b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import TypeAlias, TypedDict + +from ..shared_params.string_filter import StringFilter +from ..shared_params.datetime_filter import DatetimeFilter + +__all__ = ["VirtualModelFilterParam", "DefaultModelEntity", "Name"] + +DefaultModelEntity: TypeAlias = Union[StringFilter, str] + +Name: TypeAlias = Union[StringFilter, str] + + +class VirtualModelFilterParam(TypedDict, total=False): + """Filter for VirtualModel list queries.""" + + autoprovisioned: bool + """Filter by whether the VirtualModel is controller-managed.""" + + created_at: DatetimeFilter + """Filter by creation date.""" + + default_model_entity: DefaultModelEntity + """Filter by default model entity.""" + + name: Name + """Filter by name.""" + + project: str + """Filter by project URN.""" + + updated_at: DatetimeFilter + """Filter by update date.""" + + workspace: str + """Filter by workspace.""" diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py index 139f5b96e9..d6c86630a9 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py @@ -19,12 +19,26 @@ from typing_extensions import TypedDict +from .virtual_model_filter_param import VirtualModelFilterParam + __all__ = ["VirtualModelListParams"] class VirtualModelListParams(TypedDict, total=False): workspace: str + filter: VirtualModelFilterParam + """ + Filter virtual models by workspace, project, name, default_model_entity, + autoprovisioned, created_at, and updated_at. + """ + + include_autoprovisioned: bool + """ + When false, controller-managed (autoprovisioned) passthrough VirtualModels are + excluded from the results. + """ + page: int """Page number (1-indexed).""" diff --git a/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py b/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py index 44a5f2626c..eda4c73950 100644 --- a/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py +++ b/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py @@ -24,6 +24,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform +from nemo_platform._utils import parse_datetime from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination from nemo_platform.types.inference import ( VirtualModel, @@ -188,6 +189,32 @@ def test_method_list(self, client: NeMoPlatform) -> None: def test_method_list_with_all_params(self, client: NeMoPlatform) -> None: virtual_model = client.inference.virtual_models.list( workspace="workspace", + filter={ + "autoprovisioned": True, + "created_at": { + "gte": parse_datetime("2019-12-27T18:11:19.117Z"), + "lte": parse_datetime("2019-12-27T18:11:19.117Z"), + }, + "default_model_entity": { + "eq": "$eq", + "in_": ["string"], + "like": "$like", + "nin": ["string"], + }, + "name": { + "eq": "$eq", + "in_": ["string"], + "like": "$like", + "nin": ["string"], + }, + "project": "project", + "updated_at": { + "gte": parse_datetime("2019-12-27T18:11:19.117Z"), + "lte": parse_datetime("2019-12-27T18:11:19.117Z"), + }, + "workspace": "workspace", + }, + include_autoprovisioned=True, page=1, page_size=1, sort="sort", @@ -533,6 +560,32 @@ async def test_method_list(self, async_client: AsyncNeMoPlatform) -> None: async def test_method_list_with_all_params(self, async_client: AsyncNeMoPlatform) -> None: virtual_model = await async_client.inference.virtual_models.list( workspace="workspace", + filter={ + "autoprovisioned": True, + "created_at": { + "gte": parse_datetime("2019-12-27T18:11:19.117Z"), + "lte": parse_datetime("2019-12-27T18:11:19.117Z"), + }, + "default_model_entity": { + "eq": "$eq", + "in_": ["string"], + "like": "$like", + "nin": ["string"], + }, + "name": { + "eq": "$eq", + "in_": ["string"], + "like": "$like", + "nin": ["string"], + }, + "project": "project", + "updated_at": { + "gte": parse_datetime("2019-12-27T18:11:19.117Z"), + "lte": parse_datetime("2019-12-27T18:11:19.117Z"), + }, + "workspace": "workspace", + }, + include_autoprovisioned=True, page=1, page_size=1, sort="sort", diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index 4fbdee99d3..4172d676e3 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -385,6 +385,7 @@ resources: middleware_call: MiddlewareCall update_virtual_model_request: UpdateVirtualModelRequest virtual_model: VirtualModel + virtual_model_filter: VirtualModelFilter virtual_model_inference_config: VirtualModelInferenceConfig virtual_models_page: VirtualModelsPage methods: diff --git a/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py b/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py index 1ea5a04081..2d57362076 100644 --- a/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py +++ b/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py @@ -26,6 +26,7 @@ from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, Query, status +from nemo_platform_plugin.filter_ops import ComparisonOperation, FilterOperator from nemo_platform_plugin.inference_middleware import ( _AUTOPROVISIONED_DESC, InferenceMiddlewareError, @@ -35,7 +36,10 @@ VirtualModelInferenceConfig, ) from nmp.common.api.common import Page, PaginationData +from nmp.common.api.parsed_filter import ParsedFilter, make_filter_dep +from nmp.common.api.utils import generate_openapi_extra_params from nmp.common.entities import EntityClient, EntityConflictError, EntityNotFoundError +from nmp.common.entities.values import DatetimeFilter, Filter, StringFilter from nmp.common.service.dependencies import get_entity_client from nmp.core.inference_gateway.api.dependencies import global_middleware_registry from nmp.core.inference_gateway.api.middleware_registry import MiddlewareRegistry @@ -49,6 +53,18 @@ MiddlewareRegistryDep = Annotated[MiddlewareRegistry, Depends(global_middleware_registry)] +class VirtualModelFilter(Filter): + """Filter for VirtualModel list queries.""" + + workspace: str | None = Field(None, description="Filter by workspace.") + project: str | None = Field(None, description="Filter by project URN.") + name: StringFilter | str | None = Field(None, description="Filter by name.") + default_model_entity: StringFilter | str | None = Field(None, description="Filter by default model entity.") + autoprovisioned: bool | None = Field(None, description="Filter by whether the VirtualModel is controller-managed.") + created_at: DatetimeFilter | None = Field(None, description="Filter by creation date.") + updated_at: DatetimeFilter | None = Field(None, description="Filter by update date.") + + def _middleware_validation_error(detail: str) -> HTTPException: return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=detail) @@ -326,6 +342,13 @@ async def create_virtual_model( status_code=status.HTTP_200_OK, response_model=Page[VirtualModel], response_model_exclude_none=True, + openapi_extra=generate_openapi_extra_params( + filter_schema=VirtualModelFilter, + filter_description=( + "Filter virtual models by workspace, project, name, default_model_entity, " + "autoprovisioned, created_at, and updated_at." + ), + ), ) async def list_virtual_models( workspace: str, @@ -336,18 +359,33 @@ async def list_virtual_models( default="-created_at", description="Sort field. Prefix with ``-`` for descending order.", ), + include_autoprovisioned: bool = Query( + default=True, + description=( + "When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results." + ), + ), + parsed_filter: ParsedFilter = Depends(make_filter_dep(VirtualModelFilter)), ) -> Page[VirtualModel]: """List VirtualModels for the given workspace. Use ``workspace=-`` to list across all workspaces accessible to the caller. """ + filter_workspace = parsed_filter.remove("workspace") or workspace + if not include_autoprovisioned: + parsed_filter.and_with( + VirtualModelFilter.translate_operation( + ComparisonOperation(operator=FilterOperator.EQ, field="autoprovisioned", value=False) + ) + ) try: result = await entity_client.list( VirtualModel, - workspace=workspace, + workspace=filter_workspace, page=page, page_size=page_size, sort=sort, + filter_operation=parsed_filter.operation, ) except HTTPException: raise @@ -364,7 +402,7 @@ async def list_virtual_models( total_pages=result.pagination.total_pages, total_results=result.pagination.total_results, ) - return Page(data=result.data, pagination=pagination, sort=sort) + return Page(data=result.data, pagination=pagination, sort=sort, filter=parsed_filter.to_response()) @router.get( diff --git a/services/core/inference-gateway/tests/unit/test_virtual_models_router.py b/services/core/inference-gateway/tests/unit/test_virtual_models_router.py index 558b8f6333..41ecb60a93 100644 --- a/services/core/inference-gateway/tests/unit/test_virtual_models_router.py +++ b/services/core/inference-gateway/tests/unit/test_virtual_models_router.py @@ -395,6 +395,25 @@ def test_list_sort_reflected_in_response(self, client: TestClient): assert resp.status_code == 200 assert resp.json()["sort"] == "created_at" + def test_list_filters_out_autoprovisioned(self, client: TestClient): + """filter[autoprovisioned][$ne]=true excludes controller-managed VirtualModels.""" + _create(client, "vm-manual", default_model_entity="default/model-a") + _create(client, "vm-auto", autoprovisioned=True, default_model_entity="default/model-b") + + resp = client.get(f"{BASE}?include_autoprovisioned=false") + assert resp.status_code == 200, resp.text + names = {item["name"] for item in resp.json()["data"]} + assert "vm-manual" in names + assert "vm-auto" not in names + + def test_list_includes_autoprovisioned_by_default(self, client: TestClient): + """Autoprovisioned VirtualModels are returned when the flag is not set.""" + _create(client, "vm-manual-2", default_model_entity="default/model-a") + _create(client, "vm-auto-2", autoprovisioned=True, default_model_entity="default/model-b") + + names = {item["name"] for item in client.get(BASE).json()["data"]} + assert {"vm-manual-2", "vm-auto-2"} <= names + # --------------------------------------------------------------------------- # Update (PATCH) diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx new file mode 100644 index 0000000000..d2ada406f7 --- /dev/null +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { VirtualModelsDataView } from '@studio/components/dataViews/VirtualModelsDataView'; +import { PLATFORM_BASE_URL } from '@studio/constants/environment'; +import { server } from '@studio/mocks/node'; +import { renderRoute, screen, waitFor } from '@studio/tests/util/render'; +import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; + +vi.mock('use-debounce', () => ({ + useDebounce: (value: unknown) => [value, { cancel: () => {}, flush: () => {} }], +})); + +const VMS_URL = `${PLATFORM_BASE_URL}/apis/inference-gateway/v2/workspaces/:workspace/virtual-models`; + +const page = (data: unknown[]) => ({ + data, + pagination: { + page: 1, + page_size: 50, + current_page_size: data.length, + total_pages: data.length ? 1 : 0, + total_results: data.length, + }, +}); + +const sampleVm = { + id: 'default/my-vm', + name: 'my-vm', + workspace: 'default', + default_model_entity: 'default/gpt-4o', + autoprovisioned: false, + request_middleware: [{ name: 'nemo-switchyard', config_type: 'translate' }], + response_middleware: [{ name: 'nemo-switchyard', config_type: 'random_routing' }], + post_response_middleware: [], + created_at: '2026-07-01T00:00:00Z', + created_by: null, + updated_at: '2026-07-01T00:00:00Z', + updated_by: null, + entity_id: 'default/my-vm', + parent: '', +}; + +const renderDataView = () => + renderRoute(, { + history: '/workspaces/default/virtual-models', + routes: [ + { + path: '/workspaces/:workspace/virtual-models', + element: , + }, + ], + }); + +describe('VirtualModelsDataView', () => { + afterEach(() => { + server.resetHandlers(); + }); + + it('renders virtual models with a middleware call count', async () => { + server.use(http.get(VMS_URL, () => HttpResponse.json(page([sampleVm])))); + renderDataView(); + + expect(await screen.findByText('my-vm')).toBeInTheDocument(); + expect(screen.getByText('default/gpt-4o')).toBeInTheDocument(); + expect(screen.getByText('2 calls')).toBeInTheDocument(); + }); + + it('requests only non-autoprovisioned virtual models', async () => { + const urls: string[] = []; + server.use( + http.get(VMS_URL, ({ request }) => { + urls.push(request.url); + return HttpResponse.json(page([sampleVm])); + }) + ); + renderDataView(); + + await waitFor(() => expect(urls.length).toBeGreaterThan(0)); + const params = new URL(urls.at(-1)!).searchParams; + expect(params.get('include_autoprovisioned')).toBe('false'); + }); + + it('shows an empty state when there are no virtual models', async () => { + server.use(http.get(VMS_URL, () => HttpResponse.json(page([])))); + renderDataView(); + + expect(await screen.findByText('No Virtual Models')).toBeInTheDocument(); + }); + + it('deletes a virtual model through the row action', async () => { + const user = userEvent.setup(); + let deleted = false; + server.use( + http.get(VMS_URL, () => HttpResponse.json(page(deleted ? [] : [sampleVm]))), + http.delete(`${VMS_URL}/:name`, () => { + deleted = true; + return new HttpResponse(null, { status: 204 }); + }) + ); + renderDataView(); + + await screen.findByText('my-vm'); + + await user.click(screen.getByRole('button', { name: 'Row Actions' })); + await user.click(await screen.findByText('Delete')); + await user.click(await screen.findByRole('button', { name: 'Delete' })); + + await waitFor(() => expect(deleted).toBe(true)); + }); +}); diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx new file mode 100644 index 0000000000..6ebdcd07c8 --- /dev/null +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + ROW_ACTIONS_COLUMN_SIZE, + StudioDataView, +} from '@nemo/common/src/components/DataView/StudioDataView'; +import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; +import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; +import { useDeferredUnmount } from '@nemo/common/src/hooks/useDeferredUnmount'; +import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; +import { useToast } from '@nemo/common/src/providers/toast/useToast'; +import { + getListVirtualModelsQueryKey, + useDeleteVirtualModel, + useListVirtualModels, +} from '@nemo/sdk/generated/platform/api'; +import { VirtualModel } from '@nemo/sdk/generated/platform/schema'; +import { Flex, Stack, StatusMessage, Text } from '@nvidia/foundations-react-core'; +import { getErrorMessage } from '@studio/api/common/utils'; +import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal'; +import { ErrorPanel } from '@studio/components/ErrorPanel'; +import { VirtualModelDetailsSidePanel } from '@studio/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel'; +import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; +import { Waypoints } from 'lucide-react'; +import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; + +export interface VirtualModelsDataViewProps { + workspace: string; + attributes?: { + Stack?: React.ComponentProps; + }; +} + +type VirtualModelWithId = VirtualModel & { id: string }; + +const middlewareCount = (vm: VirtualModel): number => + (vm.request_middleware?.length ?? 0) + + (vm.response_middleware?.length ?? 0) + + (vm.post_response_middleware?.length ?? 0); + +export const VirtualModelsDataView: FC = ({ + workspace, + attributes, +}) => { + const toast = useToast(); + const queryClient = useQueryClient(); + + const { + isOpen: isDetailsPanelOpen, + value: vmForDetails, + open: openDetailsPanel, + close: closeDetailsPanel, + } = useDeferredUnmount({ delay: 300 }); + + const dataViewState = useStudioDataViewState({ + defaultSort: { id: 'created_at', desc: true }, + }); + + const [modalVirtualModel, setModalVirtualModel] = useState(); + + const sortState = dataViewState.sorting.state[0]; + const sortParam = sortState + ? sortState.desc + ? `-${sortState.id}` + : sortState.id + : '-created_at'; + + const { data, isFetching, error } = useListVirtualModels( + workspace, + { + page: dataViewState.pagination.state.pageIndex + 1, + page_size: dataViewState.pagination.state.pageSize, + sort: sortParam, + // Autoprovisioned VMs are controller-managed passthroughs; hide them from the UI. + include_autoprovisioned: false, + }, + { + query: { + placeholderData: keepPreviousData, + }, + } + ); + + const deleteVirtualModelMutation = useDeleteVirtualModel({ + mutation: { + onSuccess: () => { + toast.success('Virtual model deleted successfully.'); + queryClient.invalidateQueries({ + queryKey: getListVirtualModelsQueryKey(workspace), + }); + }, + }, + }); + + const virtualModels = useMemo(() => data?.data ?? [], [data?.data]); + const pagination = data?.pagination; + + const virtualModelsWithId = useMemo( + () => + virtualModels.map((vm: VirtualModel) => ({ + ...vm, + id: `${vm.workspace}/${vm.name}`, + })), + [virtualModels] + ); + + const handleDeleteVirtualModel = async () => { + if (!modalVirtualModel?.name) return false; + try { + await deleteVirtualModelMutation.mutateAsync({ + workspace, + name: modalVirtualModel.name, + }); + return true; + } catch { + toast.error('Failed to delete virtual model'); + return false; + } + }; + + const handleModalClose = () => { + setModalVirtualModel(undefined); + }; + + const makeColumns: ComponentProps>['makeColumns'] = + useCallback( + ({ accessor }, { rowActionsColumn }) => [ + accessor('name', { + header: 'Name', + enableSorting: true, + size: 200, + cell({ row }) { + return {row.original.name}; + }, + }), + accessor('default_model_entity', { + header: 'Default model', + enableSorting: false, + cell({ row }) { + const value = row.original.default_model_entity; + return ( + + {value || '—'} + + ); + }, + }), + accessor((vm) => middlewareCount(vm), { + id: 'middleware', + header: 'Middleware', + enableSorting: false, + size: 120, + cell({ row }) { + const count = middlewareCount(row.original); + return {count === 0 ? 'None' : `${count} call${count === 1 ? '' : 's'}`}; + }, + }), + accessor('created_at', { + header: 'Created', + enableSorting: true, + size: 150, + cell({ row }) { + return row.original.created_at ? ( + + ) : ( + + ); + }, + }), + rowActionsColumn({ + size: ROW_ACTIONS_COLUMN_SIZE, + enableResizing: false, + cellProps: { + attributes: { + DropdownContent: { className: 'min-w-[156px]' }, + }, + }, + rowActions: (vm: VirtualModelWithId) => [ + { + children: 'View', + onSelect: () => openDetailsPanel(vm), + }, + { + children: 'Delete', + danger: true, + onSelect: () => setModalVirtualModel(vm), + }, + ], + }), + ], + [openDetailsPanel] + ); + + const isInitialEmpty = virtualModelsWithId.length === 0 && !isFetching && !error; + + const emptyState = ( + + } + /> + + ); + + return ( + + openDetailsPanel(row)} + attributes={{ + DataViewRoot: { + data: virtualModelsWithId, + totalCount: pagination?.total_results, + requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, + }, + DataViewTableContent: { + renderEmptyState: () => + isInitialEmpty ? ( + emptyState + ) : ( + + ), + renderErrorState: () => ( + + ), + }, + }} + /> + + {modalVirtualModel && ( + + )} + + {vmForDetails != null && ( + + )} + + ); +}; diff --git a/web/packages/studio/src/constants/routes.ts b/web/packages/studio/src/constants/routes.ts index 8f0ab7d645..079b68bb96 100644 --- a/web/packages/studio/src/constants/routes.ts +++ b/web/packages/studio/src/constants/routes.ts @@ -88,6 +88,7 @@ export const ROUTES = { */ filesetDetail: `/workspaces/:${P.workspace}/filesets/:${P.filesetName}/detail`, inferenceProviders: `/workspaces/:${P.workspace}/inference-providers`, + virtualModels: `/workspaces/:${P.workspace}/virtual-models`, deploymentConfigs: `/workspaces/:${P.workspace}/deployment-configs`, deployments: `/workspaces/:${P.workspace}/deployments`, /** Deployments list with details side panel (deployment name + panel segment, e.g. `details`). */ diff --git a/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.test.tsx b/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.test.tsx new file mode 100644 index 0000000000..f34a652d8e --- /dev/null +++ b/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.test.tsx @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { VirtualModel } from '@nemo/sdk/generated/platform/schema'; +import { VirtualModelDetailsSidePanel } from '@studio/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel'; +import { renderRoute, screen } from '@studio/tests/util/render'; + +const vm: VirtualModel = { + id: 'default/my-vm', + name: 'my-vm', + workspace: 'default', + default_model_entity: 'default/gpt-4o', + autoprovisioned: true, + override_proxy: 'example-plugin.my-proxy', + models: [{ model: 'default/gpt-4o', backend_format: null }], + request_middleware: [ + { name: 'nemo-switchyard', config_type: 'translate', config: { target_format: 'anthropic' } }, + ], + response_middleware: [], + post_response_middleware: [], + created_at: '2026-07-01T00:00:00Z', + created_by: null, + updated_at: '2026-07-01T00:00:00Z', + updated_by: null, + entity_id: 'default/my-vm', + parent: '', +}; + +describe('VirtualModelDetailsSidePanel', () => { + it('renders core fields and middleware config read-only', () => { + renderRoute( {}} />); + + expect(screen.getByText('my-vm')).toBeInTheDocument(); + expect(screen.getAllByText('default/gpt-4o').length).toBeGreaterThan(0); + expect(screen.getByText('example-plugin.my-proxy')).toBeInTheDocument(); + expect(screen.getByText('nemo-switchyard')).toBeInTheDocument(); + expect(screen.getByText('translate')).toBeInTheDocument(); + expect(screen.getByText(/target_format/)).toBeInTheDocument(); + }); + + it('shows "None" for empty middleware pipelines', () => { + renderRoute( {}} />); + + expect(screen.getByText('Post-response')).toBeInTheDocument(); + expect(screen.getAllByText('None').length).toBeGreaterThan(0); + }); +}); diff --git a/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx b/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx new file mode 100644 index 0000000000..8c70179cb6 --- /dev/null +++ b/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { KVPair } from '@nemo/common/src/components/KVPair'; +import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; +import { MiddlewareCall, VirtualModel } from '@nemo/sdk/generated/platform/schema'; +import { Block, SidePanel, Stack, Text } from '@nvidia/foundations-react-core'; +import { FC } from 'react'; + +const MiddlewareCallView: FC<{ call: MiddlewareCall }> = ({ call }) => ( + + + + + {call.config_id ? ( + + ) : null} + {call.config && Object.keys(call.config).length > 0 ? ( + + + Config + +
+            {JSON.stringify(call.config, null, 2)}
+          
+
+ ) : null} +
+
+); + +const MiddlewarePipeline: FC<{ label: string; calls: MiddlewareCall[] | undefined }> = ({ + label, + calls, +}) => ( + + {label} + {calls && calls.length > 0 ? ( + calls.map((call, index) => ) + ) : ( + + None + + )} + +); + +export interface VirtualModelDetailsSidePanelProps { + open: boolean; + onClose: () => void; + virtualModel: VirtualModel; +} + +export const VirtualModelDetailsSidePanel: FC = ({ + open, + onClose, + virtualModel, +}) => { + const models = virtualModel.models ?? []; + + return ( + + {virtualModel.name} + + } + onOpenChange={(nextOpen) => { + if (!nextOpen) { + onClose(); + } + }} + > + + + + ) : ( + '—' + ) + } + /> + + + {virtualModel.override_proxy ? ( + + ) : null} + 0 + ? models + .map((m) => (m.backend_format ? `${m.model} (${m.backend_format})` : m.model)) + .join('\n') + : '—' + } + /> + + + + Middleware + + + + + + + ); +}; diff --git a/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx b/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx new file mode 100644 index 0000000000..db6475a4d6 --- /dev/null +++ b/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { PageHeader, Stack } from '@nvidia/foundations-react-core'; +import { AccessibleTitle } from '@studio/components/AccessibleTitle'; +import { VirtualModelsDataView } from '@studio/components/dataViews/VirtualModelsDataView'; +import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; +import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; +import { getWorkspaceVirtualModelsRoute } from '@studio/routes/utils'; +import { FC } from 'react'; + +export const VirtualModelsListRoute: FC = () => { + const workspace = useWorkspaceFromPath(); + + useBreadcrumbs({ + items: [ + { + href: getWorkspaceVirtualModelsRoute(workspace), + slotLabel: 'Virtual Models', + }, + ], + }); + + return ( + + + + + + + ); +}; diff --git a/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx b/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx index ab2574742d..febb9a77e9 100644 --- a/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx +++ b/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx @@ -42,6 +42,7 @@ import { getWorkspaceJobsRoute, getWorkspaceSafeSynthesizerRoute, getWorkspaceSettingsRoute, + getWorkspaceVirtualModelsRoute, } from '@studio/routes/utils'; import { Beaker, @@ -61,6 +62,7 @@ import { Lightbulb, Activity, FlaskConical, + Waypoints, } from 'lucide-react'; import { useMemo } from 'react'; @@ -185,6 +187,15 @@ export const WorkspaceSideNav = ({ collapsed }: { collapsed?: boolean }) => { ] : []; + const virtualModelsNav = [ + { + id: 'virtual-models', + slotIcon: , + slotLabel: 'Virtual Models', + href: getWorkspaceVirtualModelsRoute(workspace), + }, + ]; + const modelCompareNav = MODEL_COMPARE_ENABLED ? [ { @@ -242,36 +253,33 @@ export const WorkspaceSideNav = ({ collapsed }: { collapsed?: boolean }) => { }, ] : []), - ...(BASE_MODELS_ENABLED || customizerNav.length > 0 || DEPLOYMENTS_ENABLED - ? [ - { - group: 'Models', - items: [ - ...(BASE_MODELS_ENABLED - ? [ - { - id: 'models', - slotIcon: , - slotLabel: 'Base Models', - href: getWorkspaceBaseModelsRoute(workspace), - }, - ] - : []), - ...customizerNav, - ...(DEPLOYMENTS_ENABLED - ? [ - { - id: 'deployments', - slotIcon: , - slotLabel: 'Deployments', - href: getWorkspaceDeploymentsRoute(workspace), - }, - ] - : []), - ], - }, - ] - : []), + { + group: 'Models', + items: [ + ...(BASE_MODELS_ENABLED + ? [ + { + id: 'models', + slotIcon: , + slotLabel: 'Base Models', + href: getWorkspaceBaseModelsRoute(workspace), + }, + ] + : []), + ...customizerNav, + ...(DEPLOYMENTS_ENABLED + ? [ + { + id: 'deployments', + slotIcon: , + slotLabel: 'Deployments', + href: getWorkspaceDeploymentsRoute(workspace), + }, + ] + : []), + ...virtualModelsNav, + ], + }, ...(dataItems.length > 0 ? [{ group: 'Data', items: dataItems }] : []), ...(evaluateItems.length > 0 ? [{ group: 'Evaluate', items: evaluateItems }] : []), ...(safetyItems.length > 0 ? [{ group: 'Safety', items: safetyItems }] : []), diff --git a/web/packages/studio/src/routes/groups/index.ts b/web/packages/studio/src/routes/groups/index.ts index eef63bfe94..0f87e9c851 100644 --- a/web/packages/studio/src/routes/groups/index.ts +++ b/web/packages/studio/src/routes/groups/index.ts @@ -7,6 +7,7 @@ export { filesetRoutes } from '@studio/routes/groups/filesetRoutes'; export { secretsRoutes } from '@studio/routes/groups/secretsRoutes'; export { guardrailsRoutes } from '@studio/routes/groups/guardrailsRoutes'; export { inferenceProviderRoutes } from '@studio/routes/groups/inferenceProviderRoutes'; +export { virtualModelsRoutes } from '@studio/routes/groups/virtualModelsRoutes'; export { deploymentRoutes } from '@studio/routes/groups/deploymentRoutes'; export { evaluationRoutes } from '@studio/routes/groups/evaluationRoutes'; export { experimentRoutes } from '@studio/routes/groups/experimentRoutes'; diff --git a/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx b/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx new file mode 100644 index 0000000000..d81ca0bef5 --- /dev/null +++ b/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ErrorPanel } from '@studio/components/ErrorPanel'; +import { ROUTES } from '@studio/constants/routes'; +import { lazy } from 'react'; +import type { RouteObject } from 'react-router-dom'; + +const VirtualModelsListRoute = lazy(() => + import('@studio/routes/VirtualModelsListRoute').then((module) => ({ + default: module.VirtualModelsListRoute, + })) +); + +export const virtualModelsRoutes: RouteObject[] = [ + { + path: ROUTES.workspace.virtualModels, + element: , + errorElement: , + }, +]; diff --git a/web/packages/studio/src/routes/index.tsx b/web/packages/studio/src/routes/index.tsx index 55a2e6d011..36f050c827 100644 --- a/web/packages/studio/src/routes/index.tsx +++ b/web/packages/studio/src/routes/index.tsx @@ -17,6 +17,7 @@ import { filesetRoutes, guardrailsRoutes, inferenceProviderRoutes, + virtualModelsRoutes, intakeRoutes, jobRoutes, memberRoutes, @@ -104,6 +105,7 @@ export const routes: RouteObject[] = [ ...secretsRoutes, ...guardrailsRoutes, ...inferenceProviderRoutes, + ...virtualModelsRoutes, ...deploymentRoutes, ...evaluationRoutes, ...experimentRoutes, diff --git a/web/packages/studio/src/routes/utils.ts b/web/packages/studio/src/routes/utils.ts index c2877a3c60..337643aba7 100644 --- a/web/packages/studio/src/routes/utils.ts +++ b/web/packages/studio/src/routes/utils.ts @@ -205,6 +205,10 @@ export const getWorkspaceInferenceProvidersRoute = ( return base; }; +export const getWorkspaceVirtualModelsRoute = (workspace: string) => { + return generatePath(ROUTES.workspace.virtualModels, { workspace }); +}; + export const getWorkspaceDeploymentsRoute = (workspace: string) => { return generatePath(ROUTES.workspace.deployments, { workspace }); }; From f88065368ede5042ee74e3af41b5100cf3709533 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 14 Jul 2026 14:06:12 -0700 Subject: [PATCH 2/5] feat(studio): add search and filtering to Virtual Models table Add a name search bar (filter[name][$like]) plus column filters for default_model_entity (text) and created_at (date range) to the Virtual Models list, mirroring the other Studio data tables. The empty state becomes search/filter-aware with a Clear Filters action. Signed-off-by: mschwab --- .../tests/unit/test_virtual_models_router.py | 10 ++++ .../VirtualModelsDataView/index.test.tsx | 22 ++++++++- .../dataViews/VirtualModelsDataView/index.tsx | 47 +++++++++++++++++-- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/services/core/inference-gateway/tests/unit/test_virtual_models_router.py b/services/core/inference-gateway/tests/unit/test_virtual_models_router.py index 41ecb60a93..a0772e0c8c 100644 --- a/services/core/inference-gateway/tests/unit/test_virtual_models_router.py +++ b/services/core/inference-gateway/tests/unit/test_virtual_models_router.py @@ -406,6 +406,16 @@ def test_list_filters_out_autoprovisioned(self, client: TestClient): assert "vm-manual" in names assert "vm-auto" not in names + def test_list_filters_by_name_like(self, client: TestClient): + """filter[name][$like] matches VirtualModels by substring.""" + _create(client, "vm-alpha") + _create(client, "vm-beta") + + resp = client.get(f"{BASE}?filter[name][$like]=alph") + assert resp.status_code == 200, resp.text + names = {item["name"] for item in resp.json()["data"]} + assert names == {"vm-alpha"} + def test_list_includes_autoprovisioned_by_default(self, client: TestClient): """Autoprovisioned VirtualModels are returned when the flag is not set.""" _create(client, "vm-manual-2", default_model_entity="default/model-a") diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx index d2ada406f7..d01c6d1b1e 100644 --- a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx @@ -42,9 +42,9 @@ const sampleVm = { parent: '', }; -const renderDataView = () => +const renderDataViewAt = (entry: string) => renderRoute(, { - history: '/workspaces/default/virtual-models', + history: entry, routes: [ { path: '/workspaces/:workspace/virtual-models', @@ -53,6 +53,8 @@ const renderDataView = () => ], }); +const renderDataView = () => renderDataViewAt('/workspaces/default/virtual-models'); + describe('VirtualModelsDataView', () => { afterEach(() => { server.resetHandlers(); @@ -82,6 +84,22 @@ describe('VirtualModelsDataView', () => { expect(params.get('include_autoprovisioned')).toBe('false'); }); + it('sends filter[name][$like] when a name search is active', async () => { + const urls: string[] = []; + server.use( + http.get(VMS_URL, ({ request }) => { + urls.push(request.url); + return HttpResponse.json(page([sampleVm])); + }) + ); + renderDataViewAt('/workspaces/default/virtual-models?s=my-vm'); + + await waitFor(() => + expect(urls.some((u) => new URL(u).searchParams.has('filter[name][$like]'))).toBe(true) + ); + expect(new URL(urls.at(-1)!).searchParams.get('filter[name][$like]')).toBe('my-vm'); + }); + it('shows an empty state when there are no virtual models', async () => { server.use(http.get(VMS_URL, () => HttpResponse.json(page([])))); renderDataView(); diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx index 6ebdcd07c8..23b8ab7f85 100644 --- a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { withOperators } from '@nemo/common/src/api/filterOperators'; +import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { ROW_ACTIONS_COLUMN_SIZE, StudioDataView, @@ -15,8 +17,12 @@ import { useDeleteVirtualModel, useListVirtualModels, } from '@nemo/sdk/generated/platform/api'; -import { VirtualModel } from '@nemo/sdk/generated/platform/schema'; -import { Flex, Stack, StatusMessage, Text } from '@nvidia/foundations-react-core'; +import { + DatetimeFilter, + VirtualModel, + VirtualModelFilter, +} from '@nemo/sdk/generated/platform/schema'; +import { Button, Flex, Stack, StatusMessage, Text } from '@nvidia/foundations-react-core'; import { getErrorMessage } from '@studio/api/common/utils'; import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal'; import { ErrorPanel } from '@studio/components/ErrorPanel'; @@ -66,12 +72,25 @@ export const VirtualModelsDataView: FC = ({ : sortState.id : '-created_at'; + const filter = useMemo(() => { + const columnFilters = new Map(dataViewState.debouncedColumnFilters.map((f) => [f.id, f.value])); + const name = dataViewState.debouncedSearchBar || undefined; + const defaultModelEntity = columnFilters.get('default_model_entity') as string | undefined; + const createdAt = columnFilters.get('created_at') as DatetimeFilter | undefined; + return withOperators({ + ...(name ? { name: { $like: name } } : {}), + ...(defaultModelEntity ? { default_model_entity: { $like: defaultModelEntity } } : {}), + ...(createdAt ? { created_at: createdAt } : {}), + }); + }, [dataViewState.debouncedSearchBar, dataViewState.debouncedColumnFilters]); + const { data, isFetching, error } = useListVirtualModels( workspace, { page: dataViewState.pagination.state.pageIndex + 1, page_size: dataViewState.pagination.state.pageSize, sort: sortParam, + filter, // Autoprovisioned VMs are controller-managed passthroughs; hide them from the UI. include_autoprovisioned: false, }, @@ -137,6 +156,9 @@ export const VirtualModelsDataView: FC = ({ accessor('default_model_entity', { header: 'Default model', enableSorting: false, + meta: { + filter: { type: 'text', label: 'Default Model' }, + }, cell({ row }) { const value = row.original.default_model_entity; return ( @@ -160,6 +182,9 @@ export const VirtualModelsDataView: FC = ({ header: 'Created', enableSorting: true, size: 150, + meta: { + filter: dateTimeFilter('Created At'), + }, cell({ row }) { return row.original.created_at ? ( @@ -192,7 +217,10 @@ export const VirtualModelsDataView: FC = ({ [openDetailsPanel] ); - const isInitialEmpty = virtualModelsWithId.length === 0 && !isFetching && !error; + const hasSearchOrFilters = + !!dataViewState.debouncedSearchBar || dataViewState.debouncedColumnFilters.length > 0; + const isInitialEmpty = + virtualModelsWithId.length === 0 && !isFetching && !error && !hasSearchOrFilters; const emptyState = ( = ({ openDetailsPanel(row)} attributes={{ + DataViewSearchBar: { + placeholder: 'Search by name...', + }, DataViewRoot: { data: virtualModelsWithId, totalCount: pagination?.total_results, @@ -228,8 +260,13 @@ export const VirtualModelsDataView: FC = ({ emptyState ) : ( + Clear Filters + + } /> ), renderErrorState: () => ( From afc16b7677b29ec19ad4e924611867ac1c567325 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 14 Jul 2026 14:46:09 -0700 Subject: [PATCH 3/5] fix(inference-gateway): make autoprovisioned exclusion usable from SDK and CLI Codex review surfaced two dead filtering paths: - filter[autoprovisioned]=false was advertised on the VirtualModel list filter but returned nothing: the deep-object value arrives as the string "false" while the SQLite entity store holds JSON booleans, so the exact match never hit. Drop the autoprovisioned field from VirtualModelFilter; string/date filters are unaffected. - The generated CLI rendered the boolean list flag as a positive-only switch, so with a default of true it could never exclude autoprovisioned models (--include-autoprovisioned false was rejected as an extra arg). Invert the query parameter to exclude_autoprovisioned (default false), which the server ANDs as a real boolean data.autoprovisioned == false condition. The CLI now exposes a working --exclude-autoprovisioned switch, and Studio passes exclude_autoprovisioned=true. Regenerated OpenAPI, Python SDK, and CLI. Signed-off-by: mschwab --- docs/cli/reference.mdx | 5 ++-- openapi/ga/individual/platform.openapi.yaml | 18 ++++++-------- openapi/ga/openapi.yaml | 18 ++++++-------- openapi/openapi.yaml | 18 ++++++-------- .../commands/api/inference/virtual_models.py | 22 +++++++---------- .../nemo-platform/.nmpcontext/openapi.yaml | 18 ++++++-------- .../commands/api/inference/virtual_models.py | 22 +++++++---------- .../resources/inference/virtual_models.py | 24 +++++++++---------- .../inference/virtual_model_filter_param.py | 3 --- .../inference/virtual_model_list_params.py | 12 +++++----- .../inference/test_virtual_models.py | 6 ++--- .../api/v2/virtual_models.py | 18 +++++++------- .../tests/unit/test_virtual_models_router.py | 6 ++--- .../VirtualModelsDataView/index.test.tsx | 2 +- .../dataViews/VirtualModelsDataView/index.tsx | 2 +- 15 files changed, 81 insertions(+), 113 deletions(-) diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index 1dc19bca55..c47288d6ef 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -3464,7 +3464,7 @@ nemo inference virtual-models list [OPTIONS] **Options:** * `--workspace` -* `--include-autoprovisioned`: When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. +* `--exclude-autoprovisioned`: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. * `--page `: Page number (1-indexed). * `--page-size `: Number of results per page. * `--sort`: Sort field. Prefix with `-` for descending order. @@ -3477,8 +3477,7 @@ JSON-only fields: created_at: \{gte: str, lte: str} updated_at: \{gte: str, lte: str} -Filter virtual models by workspace, project, name, default_model_entity, autoprovisioned, created_at, and updated_at. -* `--filter.autoprovisioned` +Filter virtual models by workspace, project, name, default_model_entity, created_at, and updated_at. * `--filter.default-model-entity` * `--filter.name` * `--filter.project` diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 0ddc455245..bc0343b03d 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -3024,17 +3024,17 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. - - name: include_autoprovisioned + - name: exclude_autoprovisioned in: query required: false schema: type: boolean - description: When false, controller-managed (autoprovisioned) passthrough + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. - default: true - title: Include Autoprovisioned - description: When false, controller-managed (autoprovisioned) passthrough - VirtualModels are excluded from the results. + default: false + title: Exclude Autoprovisioned + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels + are excluded from the results. - in: query name: filter style: deepObject @@ -3043,7 +3043,7 @@ paths: schema: $ref: '#/components/schemas/VirtualModelFilter' description: Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. + created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18684,10 +18684,6 @@ components: - type: string description: Filter by default model entity. title: Default Model Entity - autoprovisioned: - description: Filter by whether the VirtualModel is controller-managed. - title: Autoprovisioned - type: boolean created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 0ddc455245..bc0343b03d 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -3024,17 +3024,17 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. - - name: include_autoprovisioned + - name: exclude_autoprovisioned in: query required: false schema: type: boolean - description: When false, controller-managed (autoprovisioned) passthrough + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. - default: true - title: Include Autoprovisioned - description: When false, controller-managed (autoprovisioned) passthrough - VirtualModels are excluded from the results. + default: false + title: Exclude Autoprovisioned + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels + are excluded from the results. - in: query name: filter style: deepObject @@ -3043,7 +3043,7 @@ paths: schema: $ref: '#/components/schemas/VirtualModelFilter' description: Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. + created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18684,10 +18684,6 @@ components: - type: string description: Filter by default model entity. title: Default Model Entity - autoprovisioned: - description: Filter by whether the VirtualModel is controller-managed. - title: Autoprovisioned - type: boolean created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 0ddc455245..bc0343b03d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -3024,17 +3024,17 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. - - name: include_autoprovisioned + - name: exclude_autoprovisioned in: query required: false schema: type: boolean - description: When false, controller-managed (autoprovisioned) passthrough + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. - default: true - title: Include Autoprovisioned - description: When false, controller-managed (autoprovisioned) passthrough - VirtualModels are excluded from the results. + default: false + title: Exclude Autoprovisioned + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels + are excluded from the results. - in: query name: filter style: deepObject @@ -3043,7 +3043,7 @@ paths: schema: $ref: '#/components/schemas/VirtualModelFilter' description: Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. + created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18684,10 +18684,6 @@ components: - type: string description: Filter by default model entity. title: Default Model Entity - autoprovisioned: - description: Filter by whether the VirtualModel is controller-managed. - title: Autoprovisioned - type: boolean created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py index 22ce93d794..3eb964d96e 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/virtual_models.py @@ -199,18 +199,22 @@ def delete_virtual_models( def list_virtual_models( ctx: typer.Context, workspace: Annotated[str | None, typer.Option("--workspace")] = None, + exclude_autoprovisioned: Annotated[ + bool | None, + typer.Option( + "--exclude-autoprovisioned", + help="When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results.", + ), + ] = None, filter: Annotated[ str | None, typer.Option( "--filter", metavar="FILTER_JSON", - help="Use --filter with JSON for complex/nested queries, or --filter.FIELD options for simple fields. Both can be combined, with field options taking precedence.\nJSON-only fields:\n created_at: {gte: str, lte: str}\n updated_at: {gte: str, lte: str}\n\nFilter virtual models by workspace, project, name, default_model_entity, autoprovisioned, created_at, and updated_at.", + help="Use --filter with JSON for complex/nested queries, or --filter.FIELD options for simple fields. Both can be combined, with field options taking precedence.\nJSON-only fields:\n created_at: {gte: str, lte: str}\n updated_at: {gte: str, lte: str}\n\nFilter virtual models by workspace, project, name, default_model_entity, created_at, and updated_at.", rich_help_panel="Filter Options", ), ] = None, - filter_autoprovisioned: Annotated[ - bool | None, typer.Option("--filter.autoprovisioned", rich_help_panel="Filter Options") - ] = None, filter_default_model_entity: Annotated[ str | None, typer.Option("--filter.default-model-entity", rich_help_panel="Filter Options") ] = None, @@ -219,13 +223,6 @@ def list_virtual_models( filter_workspace: Annotated[ str | None, typer.Option("--filter.workspace", rich_help_panel="Filter Options") ] = None, - include_autoprovisioned: Annotated[ - bool | None, - typer.Option( - "--include-autoprovisioned", - help="When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results.", - ), - ] = None, page: Annotated[int | None, typer.Option("--page", help="Page number (1-indexed).")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Number of results per page.")] = None, sort: Annotated[ @@ -254,15 +251,14 @@ def list_virtual_models( kwargs = build_kwargs( workspace=workspace, + exclude_autoprovisioned=exclude_autoprovisioned, filter=merge_filter_dict( filter, - autoprovisioned=filter_autoprovisioned, default_model_entity=filter_default_model_entity, name=filter_name, project=filter_project, workspace=filter_workspace, ), - include_autoprovisioned=include_autoprovisioned, page=page, page_size=page_size, sort=sort, diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 0ddc455245..bc0343b03d 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -3024,17 +3024,17 @@ paths: default: -created_at title: Sort description: Sort field. Prefix with ``-`` for descending order. - - name: include_autoprovisioned + - name: exclude_autoprovisioned in: query required: false schema: type: boolean - description: When false, controller-managed (autoprovisioned) passthrough + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. - default: true - title: Include Autoprovisioned - description: When false, controller-managed (autoprovisioned) passthrough - VirtualModels are excluded from the results. + default: false + title: Exclude Autoprovisioned + description: When true, controller-managed (autoprovisioned) passthrough VirtualModels + are excluded from the results. - in: query name: filter style: deepObject @@ -3043,7 +3043,7 @@ paths: schema: $ref: '#/components/schemas/VirtualModelFilter' description: Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. + created_at, and updated_at. responses: '200': description: Paginated list of virtual models @@ -18684,10 +18684,6 @@ components: - type: string description: Filter by default model entity. title: Default Model Entity - autoprovisioned: - description: Filter by whether the VirtualModel is controller-managed. - title: Autoprovisioned - type: boolean created_at: allOf: - $ref: '#/components/schemas/DatetimeFilter' diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py index 87c04edb04..ea1b6a2336 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/virtual_models.py @@ -199,18 +199,22 @@ def delete_virtual_models( def list_virtual_models( ctx: typer.Context, workspace: Annotated[str | None, typer.Option("--workspace")] = None, + exclude_autoprovisioned: Annotated[ + bool | None, + typer.Option( + "--exclude-autoprovisioned", + help="When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results.", + ), + ] = None, filter: Annotated[ str | None, typer.Option( "--filter", metavar="FILTER_JSON", - help="Use --filter with JSON for complex/nested queries, or --filter.FIELD options for simple fields. Both can be combined, with field options taking precedence.\nJSON-only fields:\n created_at: {gte: str, lte: str}\n updated_at: {gte: str, lte: str}\n\nFilter virtual models by workspace, project, name, default_model_entity, autoprovisioned, created_at, and updated_at.", + help="Use --filter with JSON for complex/nested queries, or --filter.FIELD options for simple fields. Both can be combined, with field options taking precedence.\nJSON-only fields:\n created_at: {gte: str, lte: str}\n updated_at: {gte: str, lte: str}\n\nFilter virtual models by workspace, project, name, default_model_entity, created_at, and updated_at.", rich_help_panel="Filter Options", ), ] = None, - filter_autoprovisioned: Annotated[ - bool | None, typer.Option("--filter.autoprovisioned", rich_help_panel="Filter Options") - ] = None, filter_default_model_entity: Annotated[ str | None, typer.Option("--filter.default-model-entity", rich_help_panel="Filter Options") ] = None, @@ -219,13 +223,6 @@ def list_virtual_models( filter_workspace: Annotated[ str | None, typer.Option("--filter.workspace", rich_help_panel="Filter Options") ] = None, - include_autoprovisioned: Annotated[ - bool | None, - typer.Option( - "--include-autoprovisioned", - help="When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results.", - ), - ] = None, page: Annotated[int | None, typer.Option("--page", help="Page number (1-indexed).")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Number of results per page.")] = None, sort: Annotated[ @@ -254,15 +251,14 @@ def list_virtual_models( kwargs = build_kwargs( workspace=workspace, + exclude_autoprovisioned=exclude_autoprovisioned, filter=merge_filter_dict( filter, - autoprovisioned=filter_autoprovisioned, default_model_entity=filter_default_model_entity, name=filter_name, project=filter_project, workspace=filter_workspace, ), - include_autoprovisioned=include_autoprovisioned, page=page, page_size=page_size, sort=sort, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py b/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py index 0e42a4d80f..24494a9f3a 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/inference/virtual_models.py @@ -213,8 +213,8 @@ def list( self, *, workspace: str | None = None, + exclude_autoprovisioned: bool | Omit = omit, filter: VirtualModelFilterParam | Omit = omit, - include_autoprovisioned: bool | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, sort: str | Omit = omit, @@ -231,12 +231,12 @@ def list( Use `workspace=-` to list across all workspaces accessible to the caller. Args: - filter: Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. - - include_autoprovisioned: When false, controller-managed (autoprovisioned) passthrough VirtualModels are + exclude_autoprovisioned: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. + filter: Filter virtual models by workspace, project, name, default_model_entity, + created_at, and updated_at. + page: Page number (1-indexed). page_size: Number of results per page. @@ -265,8 +265,8 @@ def list( timeout=timeout, query=maybe_transform( { + "exclude_autoprovisioned": exclude_autoprovisioned, "filter": filter, - "include_autoprovisioned": include_autoprovisioned, "page": page, "page_size": page_size, "sort": sort, @@ -583,8 +583,8 @@ def list( self, *, workspace: str | None = None, + exclude_autoprovisioned: bool | Omit = omit, filter: VirtualModelFilterParam | Omit = omit, - include_autoprovisioned: bool | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, sort: str | Omit = omit, @@ -601,12 +601,12 @@ def list( Use `workspace=-` to list across all workspaces accessible to the caller. Args: - filter: Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. - - include_autoprovisioned: When false, controller-managed (autoprovisioned) passthrough VirtualModels are + exclude_autoprovisioned: When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results. + filter: Filter virtual models by workspace, project, name, default_model_entity, + created_at, and updated_at. + page: Page number (1-indexed). page_size: Number of results per page. @@ -635,8 +635,8 @@ def list( timeout=timeout, query=maybe_transform( { + "exclude_autoprovisioned": exclude_autoprovisioned, "filter": filter, - "include_autoprovisioned": include_autoprovisioned, "page": page, "page_size": page_size, "sort": sort, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py index eacc631a04..14a17cfa34 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_filter_param.py @@ -33,9 +33,6 @@ class VirtualModelFilterParam(TypedDict, total=False): """Filter for VirtualModel list queries.""" - autoprovisioned: bool - """Filter by whether the VirtualModel is controller-managed.""" - created_at: DatetimeFilter """Filter by creation date.""" diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py index d6c86630a9..6bad0a298b 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/inference/virtual_model_list_params.py @@ -27,16 +27,16 @@ class VirtualModelListParams(TypedDict, total=False): workspace: str - filter: VirtualModelFilterParam + exclude_autoprovisioned: bool """ - Filter virtual models by workspace, project, name, default_model_entity, - autoprovisioned, created_at, and updated_at. + When true, controller-managed (autoprovisioned) passthrough VirtualModels are + excluded from the results. """ - include_autoprovisioned: bool + filter: VirtualModelFilterParam """ - When false, controller-managed (autoprovisioned) passthrough VirtualModels are - excluded from the results. + Filter virtual models by workspace, project, name, default_model_entity, + created_at, and updated_at. """ page: int diff --git a/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py b/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py index eda4c73950..f2ce1cf4e6 100644 --- a/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py +++ b/sdk/python/nemo-platform/tests/api_resources/inference/test_virtual_models.py @@ -189,8 +189,8 @@ def test_method_list(self, client: NeMoPlatform) -> None: def test_method_list_with_all_params(self, client: NeMoPlatform) -> None: virtual_model = client.inference.virtual_models.list( workspace="workspace", + exclude_autoprovisioned=True, filter={ - "autoprovisioned": True, "created_at": { "gte": parse_datetime("2019-12-27T18:11:19.117Z"), "lte": parse_datetime("2019-12-27T18:11:19.117Z"), @@ -214,7 +214,6 @@ def test_method_list_with_all_params(self, client: NeMoPlatform) -> None: }, "workspace": "workspace", }, - include_autoprovisioned=True, page=1, page_size=1, sort="sort", @@ -560,8 +559,8 @@ async def test_method_list(self, async_client: AsyncNeMoPlatform) -> None: async def test_method_list_with_all_params(self, async_client: AsyncNeMoPlatform) -> None: virtual_model = await async_client.inference.virtual_models.list( workspace="workspace", + exclude_autoprovisioned=True, filter={ - "autoprovisioned": True, "created_at": { "gte": parse_datetime("2019-12-27T18:11:19.117Z"), "lte": parse_datetime("2019-12-27T18:11:19.117Z"), @@ -585,7 +584,6 @@ async def test_method_list_with_all_params(self, async_client: AsyncNeMoPlatform }, "workspace": "workspace", }, - include_autoprovisioned=True, page=1, page_size=1, sort="sort", diff --git a/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py b/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py index 2d57362076..2869b4333d 100644 --- a/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py +++ b/services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/virtual_models.py @@ -60,7 +60,6 @@ class VirtualModelFilter(Filter): project: str | None = Field(None, description="Filter by project URN.") name: StringFilter | str | None = Field(None, description="Filter by name.") default_model_entity: StringFilter | str | None = Field(None, description="Filter by default model entity.") - autoprovisioned: bool | None = Field(None, description="Filter by whether the VirtualModel is controller-managed.") created_at: DatetimeFilter | None = Field(None, description="Filter by creation date.") updated_at: DatetimeFilter | None = Field(None, description="Filter by update date.") @@ -345,8 +344,7 @@ async def create_virtual_model( openapi_extra=generate_openapi_extra_params( filter_schema=VirtualModelFilter, filter_description=( - "Filter virtual models by workspace, project, name, default_model_entity, " - "autoprovisioned, created_at, and updated_at." + "Filter virtual models by workspace, project, name, default_model_entity, created_at, and updated_at." ), ), ) @@ -359,10 +357,10 @@ async def list_virtual_models( default="-created_at", description="Sort field. Prefix with ``-`` for descending order.", ), - include_autoprovisioned: bool = Query( - default=True, + exclude_autoprovisioned: bool = Query( + default=False, description=( - "When false, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results." + "When true, controller-managed (autoprovisioned) passthrough VirtualModels are excluded from the results." ), ), parsed_filter: ParsedFilter = Depends(make_filter_dep(VirtualModelFilter)), @@ -372,11 +370,11 @@ async def list_virtual_models( Use ``workspace=-`` to list across all workspaces accessible to the caller. """ filter_workspace = parsed_filter.remove("workspace") or workspace - if not include_autoprovisioned: + if exclude_autoprovisioned: + # ``autoprovisioned`` lives under the entity ``data`` blob; reference it + # directly so this does not depend on VirtualModelFilter exposing the field. parsed_filter.and_with( - VirtualModelFilter.translate_operation( - ComparisonOperation(operator=FilterOperator.EQ, field="autoprovisioned", value=False) - ) + ComparisonOperation(operator=FilterOperator.EQ, field="data.autoprovisioned", value=False) ) try: result = await entity_client.list( diff --git a/services/core/inference-gateway/tests/unit/test_virtual_models_router.py b/services/core/inference-gateway/tests/unit/test_virtual_models_router.py index a0772e0c8c..63d694055b 100644 --- a/services/core/inference-gateway/tests/unit/test_virtual_models_router.py +++ b/services/core/inference-gateway/tests/unit/test_virtual_models_router.py @@ -395,12 +395,12 @@ def test_list_sort_reflected_in_response(self, client: TestClient): assert resp.status_code == 200 assert resp.json()["sort"] == "created_at" - def test_list_filters_out_autoprovisioned(self, client: TestClient): - """filter[autoprovisioned][$ne]=true excludes controller-managed VirtualModels.""" + def test_list_excludes_autoprovisioned(self, client: TestClient): + """exclude_autoprovisioned=true excludes controller-managed VirtualModels.""" _create(client, "vm-manual", default_model_entity="default/model-a") _create(client, "vm-auto", autoprovisioned=True, default_model_entity="default/model-b") - resp = client.get(f"{BASE}?include_autoprovisioned=false") + resp = client.get(f"{BASE}?exclude_autoprovisioned=true") assert resp.status_code == 200, resp.text names = {item["name"] for item in resp.json()["data"]} assert "vm-manual" in names diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx index d01c6d1b1e..8fb4ea2a98 100644 --- a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.test.tsx @@ -81,7 +81,7 @@ describe('VirtualModelsDataView', () => { await waitFor(() => expect(urls.length).toBeGreaterThan(0)); const params = new URL(urls.at(-1)!).searchParams; - expect(params.get('include_autoprovisioned')).toBe('false'); + expect(params.get('exclude_autoprovisioned')).toBe('true'); }); it('sends filter[name][$like] when a name search is active', async () => { diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx index 23b8ab7f85..bc3b4e0584 100644 --- a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx @@ -92,7 +92,7 @@ export const VirtualModelsDataView: FC = ({ sort: sortParam, filter, // Autoprovisioned VMs are controller-managed passthroughs; hide them from the UI. - include_autoprovisioned: false, + exclude_autoprovisioned: true, }, { query: { From d23b44ff63e47e8f24dc28c44f0b04392e5a9347 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 14 Jul 2026 15:15:40 -0700 Subject: [PATCH 4/5] test(studio): exempt always-on Virtual Models route from flag-gating invariant The Virtual Models route ships without a feature flag, so the all-flags-disabled safety-net test must allow it explicitly. Every other workspace route stays gated. Signed-off-by: mschwab --- web/packages/studio/src/routes/index.test.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/packages/studio/src/routes/index.test.tsx b/web/packages/studio/src/routes/index.test.tsx index 08a8d9030b..4c5ffb4aff 100644 --- a/web/packages/studio/src/routes/index.test.tsx +++ b/web/packages/studio/src/routes/index.test.tsx @@ -111,8 +111,15 @@ describe('Routes', () => { it('every workspace route except the default route is behind a feature flag', () => { const defaultRoute = getWorkspaceDetailsDefaultRoute(WORKSPACE_ROUTE_PLACEHOLDER); + // Intentionally always-on (ungated) workspace routes. Keep this list tiny — + // new features should be flag-gated, not added here. + const alwaysOnWorkspaceRoutes = new Set([ROUTES.workspace.virtualModels]); const remainingWorkspacePaths = [ - ...new Set(collectAllPaths(routes).filter(isWorkspaceScopedPath)), + ...new Set( + collectAllPaths(routes) + .filter(isWorkspaceScopedPath) + .filter((path) => !alwaysOnWorkspaceRoutes.has(path)) + ), ].sort(); expect(remainingWorkspacePaths).toEqual([defaultRoute]); From 14c7c9716b88c443ea954f6c2b457bb2d10069f0 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 14 Jul 2026 15:23:58 -0700 Subject: [PATCH 5/5] style(studio): use import type for type-only Virtual Models imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses a CodeRabbit nitpick — FC, ComponentProps, and generated schema symbols are type-only; import them with import type per the repo TS guideline. Signed-off-by: mschwab --- .../src/components/dataViews/VirtualModelsDataView/index.tsx | 4 ++-- .../VirtualModelDetailsSidePanel/index.tsx | 4 ++-- .../studio/src/routes/VirtualModelsListRoute/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx index bc3b4e0584..ca76872cab 100644 --- a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx @@ -17,7 +17,7 @@ import { useDeleteVirtualModel, useListVirtualModels, } from '@nemo/sdk/generated/platform/api'; -import { +import type { DatetimeFilter, VirtualModel, VirtualModelFilter, @@ -29,7 +29,7 @@ import { ErrorPanel } from '@studio/components/ErrorPanel'; import { VirtualModelDetailsSidePanel } from '@studio/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel'; import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; import { Waypoints } from 'lucide-react'; -import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; +import { type ComponentProps, type FC, useCallback, useMemo, useState } from 'react'; export interface VirtualModelsDataViewProps { workspace: string; diff --git a/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx b/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx index 8c70179cb6..d78c3554a4 100644 --- a/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx +++ b/web/packages/studio/src/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel/index.tsx @@ -3,9 +3,9 @@ import { KVPair } from '@nemo/common/src/components/KVPair'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { MiddlewareCall, VirtualModel } from '@nemo/sdk/generated/platform/schema'; +import type { MiddlewareCall, VirtualModel } from '@nemo/sdk/generated/platform/schema'; import { Block, SidePanel, Stack, Text } from '@nvidia/foundations-react-core'; -import { FC } from 'react'; +import type { FC } from 'react'; const MiddlewareCallView: FC<{ call: MiddlewareCall }> = ({ call }) => ( diff --git a/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx b/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx index db6475a4d6..ad5c9bcf2b 100644 --- a/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx +++ b/web/packages/studio/src/routes/VirtualModelsListRoute/index.tsx @@ -7,7 +7,7 @@ import { VirtualModelsDataView } from '@studio/components/dataViews/VirtualModel import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; import { getWorkspaceVirtualModelsRoute } from '@studio/routes/utils'; -import { FC } from 'react'; +import type { FC } from 'react'; export const VirtualModelsListRoute: FC = () => { const workspace = useWorkspaceFromPath();