Skip to content

Commit a76d8c5

Browse files
Merge pull request #801 from kubeflow/main
[pull] main from kubeflow:main
2 parents ca7a312 + 7bbb7e1 commit a76d8c5

9 files changed

Lines changed: 387 additions & 612 deletions

File tree

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_test.go

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ func TestLoadCatalogSources(t *testing.T) {
2929
{
3030
name: "test-catalog-sources",
3131
args: args{catalogsPath: "testdata/test-catalog-sources.yaml"},
32-
want: []string{"catalog1"},
33-
wantErr: false,
32+
want: []string{"catalog1", "catalog2"},
3433
},
3534
}
3635
for _, tt := range tests {
@@ -63,6 +62,7 @@ func TestLoadCatalogSources(t *testing.T) {
6362

6463
func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) {
6564
trueValue := true
65+
falseValue := false
6666
type args struct {
6767
catalogsPath string
6868
}
@@ -82,6 +82,12 @@ func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) {
8282
Enabled: &trueValue,
8383
Labels: []string{},
8484
},
85+
"catalog2": {
86+
Id: "catalog2",
87+
Name: "Catalog 2",
88+
Enabled: &falseValue,
89+
Labels: []string{},
90+
},
8591
},
8692
wantErr: false,
8793
},
@@ -470,6 +476,87 @@ func TestLoadCatalogSourcesWithRepositoryErrors(t *testing.T) {
470476
}
471477
}
472478

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+
473560
func TestMockRepositoryBehavior(t *testing.T) {
474561
mockRepo := &MockCatalogModelRepository{}
475562

catalog/internal/catalog/hf_catalog.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -633,21 +633,31 @@ func newHFModelProvider(ctx context.Context, source *Source, reldir string) (<-c
633633
}
634634
apiKey := os.Getenv(apiKeyEnvVar)
635635
if apiKey == "" {
636-
return nil, fmt.Errorf("missing %s environment variable for HuggingFace catalog", apiKeyEnvVar)
636+
glog.Infof("No API key configured for Hugging Face. Only public models and limited data for gated models will be available.")
637637
}
638638
p.apiKey = apiKey
639639

640640
// Parse base URL (optional, defaults to huggingface.co)
641641
// This allows tests to use mock servers by providing a custom URL
642642
p.baseURL = defaultHuggingFaceURL
643643
if url, ok := source.Properties[urlKey].(string); ok && url != "" {
644-
p.baseURL = strings.TrimSuffix(url, "/") // Remove trailing slash if present
644+
p.baseURL = strings.TrimSuffix(url, "/")
645645
}
646646

647-
// Validate credentials before proceeding
648-
if err := p.validateCredentials(ctx); err != nil {
649-
glog.Errorf("HuggingFace catalog credential validation failed: %v", err)
650-
return nil, fmt.Errorf("failed to validate HuggingFace catalog credentials: %w", err)
647+
if p.apiKey != "" {
648+
hasValidPrefix := strings.HasPrefix(p.apiKey, "hf_")
649+
if !hasValidPrefix {
650+
// API key is set but doesn't have expected prefix, warn and continue without authentication
651+
glog.Infof("API key does not have expected 'hf_' prefix. Only public models and limited data for gated models will be available.")
652+
p.apiKey = "" // Clear invalid key to prevent its use
653+
}
654+
if hasValidPrefix {
655+
// Validate credentials only if API key has correct format
656+
if err := p.validateCredentials(ctx); err != nil {
657+
glog.Errorf("HuggingFace catalog credential validation failed: %v", err)
658+
return nil, fmt.Errorf("failed to validate HuggingFace catalog credentials: %w", err)
659+
}
660+
}
651661
}
652662

653663
// Use top-level IncludedModels from Source as the list of models to fetch

catalog/internal/catalog/loader.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,18 @@ func (l *Loader) readProviderRecords(ctx context.Context) <-chan ModelProviderRe
330330
ch := make(chan ModelProviderRecord)
331331
var wg sync.WaitGroup
332332

333-
// Get all enabled sources from the merged collection.
333+
// Get all sources from the merged collection.
334334
// This allows sparse overrides to work: a user can enable a disabled source
335335
// with just "id" and "enabled: true", inheriting Type and Properties from the base.
336336
mergedSources := l.Sources.AllSources()
337337

338338
for _, source := range mergedSources {
339+
// Skip disabled sources - only load catalog data from enabled sources
340+
// Per OpenAPI spec, enabled defaults to true, so nil is treated as enabled
341+
if source.Enabled != nil && !*source.Enabled {
342+
continue
343+
}
344+
339345
// Skip sources that have already been loaded
340346
if l.loadedSources[source.Id] {
341347
continue

catalog/internal/catalog/sources.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,21 +159,19 @@ func (sc *SourceCollection) merged() map[string]Source {
159159

160160
// AllSources returns all merged sources including Type and Properties.
161161
// This is used by the loader to get complete source information.
162-
// Only enabled sources are returned.
162+
// All sources are returned regardless of enabled status.
163163
func (sc *SourceCollection) AllSources() map[string]Source {
164164
sc.mu.RLock()
165165
defer sc.mu.RUnlock()
166166

167167
result := map[string]Source{}
168168
for id, source := range sc.merged() {
169-
if source.Enabled != nil && *source.Enabled {
170-
result[id] = source
171-
}
169+
result[id] = source
172170
}
173171
return result
174172
}
175173

176-
// All returns all enabled sources as CatalogSource (for the API).
174+
// All returns all sources as CatalogSource (for the API).
177175
// This excludes internal fields like Type and Properties.
178176
func (sc *SourceCollection) All() map[string]model.CatalogSource {
179177
result := map[string]model.CatalogSource{}

catalog/internal/catalog/sources_test.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ func TestSourceCollection_MergeOverride(t *testing.T) {
412412
},
413413
},
414414
{
415-
name: "user can disable a source from default - disabled sources not returned",
415+
name: "user can disable a source from default - disabled sources ARE returned",
416416
originOrder: []string{"default.yaml", "user.yaml"},
417417
mergeSequence: []struct {
418418
origin string
@@ -441,8 +441,15 @@ func TestSourceCollection_MergeOverride(t *testing.T) {
441441
},
442442
},
443443
},
444-
// Disabled sources are not returned by All()
445-
expectedSources: map[string]model.CatalogSource{},
444+
// Disabled sources ARE returned by All() - with enabled field showing false
445+
expectedSources: map[string]model.CatalogSource{
446+
"hf": {
447+
Id: "hf",
448+
Name: "Hugging Face",
449+
Enabled: apiutils.Of(false),
450+
Labels: []string{},
451+
},
452+
},
446453
},
447454
{
448455
name: "sparse override: user enables disabled source with just id and enabled",

0 commit comments

Comments
 (0)