Skip to content

Commit 964460a

Browse files
Merge pull request #764 from opendatahub-io/main
sync: main to stable keep [1891](kubeflow#1891) keep [1959](kubeflow#1959) keep [1961](kubeflow#1961) keep [1955](kubeflow#1955) keep [1957](kubeflow#1957) keep [1918](kubeflow#1918) keep [759](#759) keep [1975](kubeflow#1975) keep [1976](kubeflow#1976) keep [1963](kubeflow#1963) keep [801](#801)
2 parents 0ad5156 + a76d8c5 commit 964460a

61 files changed

Lines changed: 8569 additions & 2385 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ catalog/pkg/openapi/model_catalog_model_artifact.go linguist-generated=true
2828
catalog/pkg/openapi/model_catalog_model_list.go linguist-generated=true
2929
catalog/pkg/openapi/model_catalog_source.go linguist-generated=true
3030
catalog/pkg/openapi/model_catalog_source_list.go linguist-generated=true
31+
catalog/pkg/openapi/model_catalog_source_preview_response.go linguist-generated=true
32+
catalog/pkg/openapi/model_catalog_source_preview_response_all_of_summary.go linguist-generated=true
3133
catalog/pkg/openapi/model_error.go linguist-generated=true
3234
catalog/pkg/openapi/model_filter_option.go linguist-generated=true
3335
catalog/pkg/openapi/model_filter_option_range.go linguist-generated=true
@@ -39,6 +41,7 @@ catalog/pkg/openapi/model_metadata_proto_value.go linguist-generated=true
3941
catalog/pkg/openapi/model_metadata_string_value.go linguist-generated=true
4042
catalog/pkg/openapi/model_metadata_struct_value.go linguist-generated=true
4143
catalog/pkg/openapi/model_metadata_value.go linguist-generated=true
44+
catalog/pkg/openapi/model_model_preview_result.go linguist-generated=true
4245
catalog/pkg/openapi/model_order_by_field.go linguist-generated=true
4346
catalog/pkg/openapi/model_sort_order.go linguist-generated=true
4447
catalog/pkg/openapi/response.go linguist-generated=true

api/openapi/catalog.yaml

Lines changed: 406 additions & 9 deletions
Large diffs are not rendered by default.

api/openapi/src/catalog.yaml

Lines changed: 408 additions & 9 deletions
Large diffs are not rendered by default.

catalog/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ The HuggingFace catalog source allows you to discover and import models from the
8282
8383
#### 1. Set Your API Key
8484
85-
The HuggingFace provider requires an API key for authentication. By default, the service reads the API key from the `HF_API_KEY` environment variable:
85+
Setting a Hugging Face API key is optional. Hugging Face requires an API key for authentication for full access to data of models that are private and/or gated. If an API key is NOT set, private models will be entirely unavailable and gated models will have limited metadata. By default, the service reads the API key from the `HF_API_KEY` environment variable:
8686

8787
```bash
8888
export HF_API_KEY="your-huggingface-api-key-here"

catalog/internal/catalog/catalog.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ type ListArtifactsParams struct {
2626
ArtifactTypesFilter []string
2727
}
2828

29+
type ListPerformanceArtifactsParams struct {
30+
FilterQuery string
31+
PageSize int32
32+
OrderBy string
33+
SortOrder model.SortOrder
34+
NextPageToken *string
35+
TargetRPS int32
36+
Recommendations bool
37+
RPSProperty string // configurable "requests_per_second"
38+
LatencyProperty string // configurable "ttft_p90"
39+
HardwareCountProperty string // configurable "hardware_count"
40+
HardwareTypeProperty string // configurable "hardware_type"
41+
}
42+
2943
// APIProvider implements the API endpoints.
3044
type APIProvider interface {
3145
// GetModel returns model metadata for a single model by its name. If
@@ -43,6 +57,12 @@ type APIProvider interface {
4357
// found, but has no artifacts, an empty list is returned.
4458
GetArtifacts(ctx context.Context, modelName string, sourceID string, params ListArtifactsParams) (model.CatalogArtifactList, error)
4559

60+
// GetPerformanceArtifacts returns all performance-metrics artifacts for a particular model.
61+
// It filters artifacts by metricsType=performance-metrics and calculates custom properties
62+
// for targetRPS when specified. If no model is found with that name, it returns nil.
63+
// If the model is found but has no performance artifacts, an empty list is returned.
64+
GetPerformanceArtifacts(ctx context.Context, modelName string, sourceID string, params ListPerformanceArtifactsParams) (model.CatalogArtifactList, error)
65+
4666
// GetFilterOptions returns all available filter options for models.
4767
// This includes field names, data types, and available values or ranges.
4868
GetFilterOptions(ctx context.Context) (*model.FilterOptionsList, error)

catalog/internal/catalog/catalog_test.go

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
apimodels "github.com/kubeflow/model-registry/catalog/pkg/openapi"
1414
"github.com/kubeflow/model-registry/internal/apiutils"
1515
mrmodels "github.com/kubeflow/model-registry/internal/db/models"
16+
"github.com/stretchr/testify/assert"
1617
)
1718

1819
func TestLoadCatalogSources(t *testing.T) {
@@ -28,8 +29,7 @@ func TestLoadCatalogSources(t *testing.T) {
2829
{
2930
name: "test-catalog-sources",
3031
args: args{catalogsPath: "testdata/test-catalog-sources.yaml"},
31-
want: []string{"catalog1"},
32-
wantErr: false,
32+
want: []string{"catalog1", "catalog2"},
3333
},
3434
}
3535
for _, tt := range tests {
@@ -62,6 +62,7 @@ func TestLoadCatalogSources(t *testing.T) {
6262

6363
func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) {
6464
trueValue := true
65+
falseValue := false
6566
type args struct {
6667
catalogsPath string
6768
}
@@ -81,6 +82,12 @@ func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) {
8182
Enabled: &trueValue,
8283
Labels: []string{},
8384
},
85+
"catalog2": {
86+
Id: "catalog2",
87+
Name: "Catalog 2",
88+
Enabled: &falseValue,
89+
Labels: []string{},
90+
},
8491
},
8592
wantErr: false,
8693
},
@@ -469,6 +476,87 @@ func TestLoadCatalogSourcesWithRepositoryErrors(t *testing.T) {
469476
}
470477
}
471478

479+
func TestLoadCatalogSourcesWithNilEnabled(t *testing.T) {
480+
// Test that nil Enabled field is treated as enabled (per OpenAPI spec default: true)
481+
mockModelRepo := &MockCatalogModelRepository{}
482+
mockArtifactRepo := &MockCatalogArtifactRepository{}
483+
mockModelArtifactRepo := &MockCatalogModelArtifactRepository{}
484+
mockMetricsArtifactRepo := &MockCatalogMetricsArtifactRepository{}
485+
486+
services := service.NewServices(
487+
mockModelRepo,
488+
mockArtifactRepo,
489+
mockModelArtifactRepo,
490+
mockMetricsArtifactRepo,
491+
&MockPropertyOptionsRepository{},
492+
)
493+
494+
// Register a test provider
495+
testProviderName := "test-nil-enabled-provider"
496+
RegisterModelProvider(testProviderName, func(ctx context.Context, source *Source, reldir string) (<-chan ModelProviderRecord, error) {
497+
ch := make(chan ModelProviderRecord, 1)
498+
499+
modelName := "test-model-nil-enabled"
500+
model := &dbmodels.CatalogModelImpl{
501+
Attributes: &dbmodels.CatalogModelAttributes{
502+
Name: &modelName,
503+
},
504+
}
505+
506+
ch <- ModelProviderRecord{
507+
Model: model,
508+
Artifacts: []dbmodels.CatalogArtifact{},
509+
}
510+
close(ch)
511+
512+
return ch, nil
513+
})
514+
515+
testConfig := &sourceConfig{
516+
Catalogs: []Source{
517+
{
518+
CatalogSource: apimodels.CatalogSource{
519+
Id: "test-catalog-nil-enabled",
520+
Name: "Test Catalog Nil Enabled",
521+
Enabled: nil, // Nil should be treated as enabled
522+
},
523+
Type: testProviderName,
524+
},
525+
},
526+
}
527+
528+
l := NewLoader(services, []string{})
529+
ctx := context.Background()
530+
531+
// First call updateSources to populate the SourceCollection
532+
err := l.updateSources("test-path", testConfig)
533+
if err != nil {
534+
t.Fatalf("updateSources() error = %v", err)
535+
}
536+
537+
err = l.updateDatabase(ctx)
538+
if err != nil {
539+
t.Fatalf("updateDatabase() error = %v", err)
540+
}
541+
542+
// Wait for processing
543+
time.Sleep(100 * time.Millisecond)
544+
545+
// Verify that the model WAS saved (because nil Enabled is treated as enabled)
546+
if len(mockModelRepo.SavedModels) != 1 {
547+
t.Errorf("Expected 1 model to be saved (nil Enabled should be treated as enabled), got %d", len(mockModelRepo.SavedModels))
548+
}
549+
550+
if len(mockModelRepo.SavedModels) > 0 {
551+
savedModel := mockModelRepo.SavedModels[0]
552+
if savedModel.GetAttributes() == nil || savedModel.GetAttributes().Name == nil {
553+
t.Error("Saved model should have attributes with name")
554+
} else if *savedModel.GetAttributes().Name != "test-model-nil-enabled" {
555+
t.Errorf("Expected model name 'test-model-nil-enabled', got '%s'", *savedModel.GetAttributes().Name)
556+
}
557+
}
558+
}
559+
472560
func TestMockRepositoryBehavior(t *testing.T) {
473561
mockRepo := &MockCatalogModelRepository{}
474562

@@ -791,3 +879,59 @@ func (m *MockPropertyOptionsRepository) SetMockOptions(t dbmodels.PropertyOption
791879
}
792880
m.MockOptions[t][typeID] = options
793881
}
882+
883+
func TestAPIProviderGetPerformanceArtifacts(t *testing.T) {
884+
// This test verifies that the APIProvider interface has GetPerformanceArtifacts method
885+
// The actual implementation is tested in db_catalog_test.go
886+
887+
// Create a mock provider to verify interface compliance
888+
services := service.NewServices(
889+
&MockCatalogModelRepository{},
890+
&MockCatalogArtifactRepository{},
891+
&MockCatalogModelArtifactRepository{},
892+
&MockCatalogMetricsArtifactRepository{},
893+
&MockPropertyOptionsRepository{},
894+
)
895+
provider := NewDBCatalog(services, nil)
896+
897+
// Verify provider implements APIProvider interface with GetPerformanceArtifacts
898+
var _ APIProvider = provider
899+
900+
// Basic test - should return error for non-existent model
901+
ctx := context.Background()
902+
_, err := provider.GetPerformanceArtifacts(ctx, "non-existent-model", "source-1", ListPerformanceArtifactsParams{
903+
TargetRPS: 100,
904+
Recommendations: true,
905+
PageSize: 10,
906+
})
907+
908+
// Should get an error since the model doesn't exist
909+
assert.Error(t, err)
910+
}
911+
912+
// TestAPIProviderInterface verifies that the APIProvider interface supports
913+
// all required fields in ListPerformanceArtifactsParams
914+
func TestAPIProviderInterface(t *testing.T) {
915+
services := service.NewServices(
916+
&MockCatalogModelRepository{},
917+
&MockCatalogArtifactRepository{},
918+
&MockCatalogModelArtifactRepository{},
919+
&MockCatalogMetricsArtifactRepository{},
920+
&MockPropertyOptionsRepository{},
921+
)
922+
var provider APIProvider = NewDBCatalog(services, nil)
923+
924+
params := ListPerformanceArtifactsParams{
925+
TargetRPS: 100,
926+
Recommendations: true,
927+
RPSProperty: "custom_rps",
928+
LatencyProperty: "custom_latency",
929+
HardwareCountProperty: "custom_hw_count",
930+
HardwareTypeProperty: "custom_hw_type",
931+
}
932+
933+
// Should compile without errors and be callable
934+
ctx := context.Background()
935+
_, err := provider.GetPerformanceArtifacts(ctx, "test-model", "source-1", params)
936+
assert.Error(t, err) // Expected error since model doesn't exist
937+
}

catalog/internal/catalog/db_catalog.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type dbCatalogImpl struct {
2323
catalogModelRepository dbmodels.CatalogModelRepository
2424
catalogArtifactRepository dbmodels.CatalogArtifactRepository
2525
propertyOptionsRepository dbmodels.PropertyOptionsRepository
26+
performanceService *dbmodels.PerformanceArtifactService
2627
sources *SourceCollection
2728
}
2829

@@ -31,6 +32,7 @@ func NewDBCatalog(services service.Services, sources *SourceCollection) APIProvi
3132
catalogArtifactRepository: services.CatalogArtifactRepository,
3233
catalogModelRepository: services.CatalogModelRepository,
3334
propertyOptionsRepository: services.PropertyOptionsRepository,
35+
performanceService: dbmodels.NewPerformanceArtifactService(services.CatalogArtifactRepository),
3436
sources: sources,
3537
}
3638
}
@@ -128,7 +130,7 @@ func (d *dbCatalogImpl) ListModels(ctx context.Context, params ListModelsParams)
128130
}
129131

130132
func (d *dbCatalogImpl) GetArtifacts(ctx context.Context, modelName string, sourceID string, params ListArtifactsParams) (apimodels.CatalogArtifactList, error) {
131-
pageSize := int32(params.PageSize)
133+
pageSize := params.PageSize
132134

133135
// Use consistent defaults to match pagination logic
134136
orderBy := string(params.OrderBy)
@@ -240,6 +242,67 @@ func (d *dbCatalogImpl) GetFilterOptions(ctx context.Context) (*apimodels.Filter
240242
}, nil
241243
}
242244

245+
func (d *dbCatalogImpl) GetPerformanceArtifacts(ctx context.Context, modelName string, sourceID string, params ListPerformanceArtifactsParams) (apimodels.CatalogArtifactList, error) {
246+
// Get the model to validate it exists and get its ID
247+
modelsList, err := d.catalogModelRepository.List(dbmodels.CatalogModelListOptions{
248+
Name: &modelName,
249+
SourceIDs: &[]string{sourceID},
250+
})
251+
if err != nil {
252+
return apimodels.CatalogArtifactList{}, err
253+
}
254+
255+
if len(modelsList.Items) == 0 {
256+
return apimodels.CatalogArtifactList{}, fmt.Errorf("no models found for name=%v: %w", modelName, api.ErrNotFound)
257+
}
258+
259+
if len(modelsList.Items) > 1 {
260+
return apimodels.CatalogArtifactList{}, fmt.Errorf("multiple models found for name=%v: %w", modelName, api.ErrNotFound)
261+
}
262+
263+
model := modelsList.Items[0]
264+
265+
serviceParams := dbmodels.PerformanceArtifactParams{
266+
ModelID: *model.GetID(),
267+
TargetRPS: params.TargetRPS,
268+
Recommendations: params.Recommendations,
269+
FilterQuery: params.FilterQuery,
270+
PageSize: params.PageSize,
271+
OrderBy: params.OrderBy,
272+
SortOrder: string(params.SortOrder),
273+
NextPageToken: params.NextPageToken,
274+
RPSProperty: params.RPSProperty,
275+
LatencyProperty: params.LatencyProperty,
276+
HardwareCountProperty: params.HardwareCountProperty,
277+
HardwareTypeProperty: params.HardwareTypeProperty,
278+
}
279+
280+
artifactsList, err := d.performanceService.GetArtifacts(serviceParams)
281+
if err != nil {
282+
return apimodels.CatalogArtifactList{}, fmt.Errorf("failed to get performance artifacts: %w", err)
283+
}
284+
285+
artifactList := &apimodels.CatalogArtifactList{
286+
Items: make([]apimodels.CatalogArtifact, 0, len(artifactsList.Items)),
287+
}
288+
289+
for _, artifact := range artifactsList.Items {
290+
mappedArtifact, err := mapDBArtifactToAPIArtifact(dbmodels.CatalogArtifact{
291+
CatalogMetricsArtifact: artifact,
292+
})
293+
if err != nil {
294+
return apimodels.CatalogArtifactList{}, err
295+
}
296+
artifactList.Items = append(artifactList.Items, mappedArtifact)
297+
}
298+
299+
artifactList.NextPageToken = artifactsList.NextPageToken
300+
artifactList.PageSize = params.PageSize
301+
artifactList.Size = int32(len(artifactList.Items))
302+
303+
return *artifactList, nil
304+
}
305+
243306
func dbPropToAPIOption(prop dbmodels.PropertyOption) *apimodels.FilterOption {
244307
var option apimodels.FilterOption
245308

0 commit comments

Comments
 (0)