Skip to content

Commit 9b49686

Browse files
authored
Hugging Face pattern matching and allowedOrganization support (#2041)
* fix(catalog): Forbid a few invalid HF patterns It's invalid to put a wildcard in the HF org (`foo*bar/`) or omit the model name (`foo/`). Signed-off-by: Paul Boyd <[email protected]> * feat(catalog): wildcard pattern support for Hugging Face Extends the Hugging Face source to support wildcard patterns like: - org/* (all models from organization) - org/prefix* (models with specific prefix) This was already supported when previewing a source. Signed-off-by: Paul Boyd <[email protected]> * feat(catalog): implement allowedOrganization for HF Signed-off-by: Paul Boyd <[email protected]> * fix(catalog): clarify HF wildcard docs Signed-off-by: Paul Boyd <[email protected]> --------- Signed-off-by: Paul Boyd <[email protected]>
1 parent 108450b commit 9b49686

File tree

5 files changed

+528
-36
lines changed

5 files changed

+528
-36
lines changed

catalog/README.md

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,33 +320,113 @@ catalogs:
320320
enabled: true
321321
# Required: List of model identifiers to include
322322
# Format: "organization/model-name" or "username/model-name"
323+
# Supports wildcard patterns: "organization/*" or "organization/prefix*"
323324
includedModels:
324325
- "meta-llama/Llama-3.1-8B-Instruct"
325-
- "ibm-granite/granite-4.0-h-small"
326326
- "microsoft/phi-2"
327-
327+
- "microsoft/phi-3*" # All models starting with "phi-3"
328+
328329
# Optional: Exclude specific models or patterns
329330
# Supports exact matches or patterns ending with "*"
330331
excludedModels:
331332
- "some-org/unwanted-model"
332333
- "another-org/test-*" # Excludes all models starting with "test-"
333-
334+
334335
# Optional: Configure a custom environment variable name for the API key
335336
# Defaults to "HF_API_KEY" if not specified
336337
properties:
337338
apiKeyEnvVar: "MY_CUSTOM_API_KEY_VAR"
338339
```
339340

341+
#### Organization-Restricted Sources
342+
343+
You can restrict a source to only fetch models from a specific organization using the `allowedOrganization` property. This automatically prefixes all model patterns with the organization name:
344+
345+
```yaml
346+
catalogs:
347+
- name: "Meta LLaMA Models"
348+
id: "meta-llama-models"
349+
type: "hf"
350+
enabled: true
351+
properties:
352+
allowedOrganization: "meta-llama"
353+
apiKeyEnvVar: "HF_API_KEY"
354+
includedModels:
355+
# These patterns are automatically prefixed with "meta-llama/"
356+
- "*" # Expands to: meta-llama/*
357+
- "Llama-3*" # Expands to: meta-llama/Llama-3*
358+
- "CodeLlama-*" # Expands to: meta-llama/CodeLlama-*
359+
excludedModels:
360+
- "*-4bit" # Excludes: meta-llama/*-4bit
361+
- "*-GGUF" # Excludes: meta-llama/*-GGUF
362+
```
363+
364+
**Benefits of organization-restricted sources:**
365+
- **Simplified configuration**: No need to repeat organization name in every pattern
366+
- **Security**: Prevents accidental inclusion of models from other organizations
367+
- **Convenience**: Use `"*"` to get all models from an organization
368+
- **Performance**: Optimized API calls when fetching from a single organization
369+
340370
#### Model Filtering
341371

342372
Both `includedModels` and `excludedModels` are top-level properties (not nested under `properties`):
343373

344-
- **`includedModels`** (required): List of model identifiers to fetch from Hugging Face. Format: `"organization/model-name"` or `"username/model-name"`
374+
- **`includedModels`** (required): List of model identifiers to fetch from Hugging Face
345375
- **`excludedModels`** (optional): List of models or patterns to exclude from the results
346376

347-
The `excludedModels` property supports:
377+
#### Supported Pattern Types
378+
379+
**Exact Model Names:**
380+
```yaml
381+
includedModels:
382+
- "meta-llama/Llama-3.1-8B-Instruct" # Specific model
383+
- "microsoft/phi-2" # Specific model
384+
```
385+
386+
**Wildcard Patterns:**
387+
388+
In `includedModels`, wildcards can match model names by a prefix.
389+
390+
```yaml
391+
includedModels:
392+
- "microsoft/phi-*" # All models starting with "phi-"
393+
- "meta-llama/Llama-3*" # All models starting with "Llama-3"
394+
- "huggingface/*" # All models from huggingface organization
395+
```
396+
397+
**Organization-Only Patterns (with `allowedOrganization`):**
398+
```yaml
399+
properties:
400+
allowedOrganization: "meta-llama"
401+
includedModels:
402+
- "*" # All models from meta-llama organization
403+
- "Llama-3*" # All meta-llama models starting with "Llama-3"
404+
- "CodeLlama-*" # All meta-llama models starting with "CodeLlama-"
405+
```
406+
407+
#### Pattern Validation
408+
409+
**Valid patterns:**
410+
- `"org/model"` - Exact model name
411+
- `"org/prefix*"` - Models starting with prefix
412+
- `"org/*"` - All models from organization
413+
- `"*"` - All models (only when using `allowedOrganization`)
414+
415+
**Invalid patterns (will be rejected):**
416+
- `"*"` - Global wildcard (without `allowedOrganization`)
417+
- `"*/*"` - Global organization wildcard
418+
- `"org*"` - Wildcard in organization name
419+
- `"org/"` - Empty model name
420+
- `"*prefix*"` - Multiple wildcards
421+
422+
#### Exclusion Patterns
423+
424+
The `excludedModels` property supports prefixes like `includedModels` and also suffixes and mid-name wildcards:
348425
- **Exact matches**: `"meta-llama/Llama-3.1-8B-Instruct"` - excludes this specific model
349-
- **Pattern matching**: `"test-*"` - excludes all models starting with "test-"
426+
- **Pattern matching**:
427+
- `"*-draft"` - excludes all models ending with "-draft"
428+
- `"Llama-3.*-Instruct"` - excludes all Llama 3.x models ending with "-Instruct"
429+
- **Organization patterns**: `"test-org/*"` - excludes all models from test-org
350430

351431
## Development
352432

catalog/internal/catalog/hf_catalog.go

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
apiKeyEnvVarKey = "apiKeyEnvVar"
2727
maxModelsKey = "maxModels"
2828
syncIntervalKey = "syncInterval"
29+
allowedOrgKey = "allowedOrganization"
2930

3031
// defaultMaxModels is the default limit for models fetched PER PATTERN.
3132
// This limit is applied independently to each pattern in includedModels
@@ -360,13 +361,72 @@ func (p *hfModelProvider) Models(ctx context.Context) (<-chan ModelProviderRecor
360361
return ch, nil
361362
}
362363

364+
// expandModelNames takes a list of model identifiers (which may include wildcards)
365+
// and returns a list of concrete model names by expanding any wildcard patterns.
366+
// Uses the same logic as FetchModelNamesForPreview.
367+
func (p *hfModelProvider) expandModelNames(ctx context.Context, modelIdentifiers []string) ([]string, error) {
368+
var allNames []string
369+
var failedPatterns []string
370+
var wildcardPatterns []string
371+
372+
for _, pattern := range modelIdentifiers {
373+
select {
374+
case <-ctx.Done():
375+
return nil, ctx.Err()
376+
default:
377+
}
378+
379+
patternType, org, searchPrefix := parseModelPattern(pattern)
380+
381+
switch patternType {
382+
case PatternInvalid:
383+
return nil, fmt.Errorf("wildcard pattern %q is not supported - Hugging Face requires a specific organization (e.g., 'ibm-granite/*' or 'meta-llama/Llama-2-*')", pattern)
384+
385+
case PatternOrgAll, PatternOrgPrefix:
386+
wildcardPatterns = append(wildcardPatterns, pattern)
387+
glog.Infof("Expanding wildcard pattern: %s (org=%s, prefix=%s)", pattern, org, searchPrefix)
388+
models, err := p.listModelsByAuthor(ctx, org, searchPrefix)
389+
if err != nil {
390+
failedPatterns = append(failedPatterns, pattern)
391+
glog.Warningf("Failed to expand wildcard pattern %s: %v", pattern, err)
392+
continue
393+
}
394+
allNames = append(allNames, models...)
395+
396+
case PatternExact:
397+
// Direct model name - no expansion needed
398+
allNames = append(allNames, pattern)
399+
}
400+
}
401+
402+
// Check error conditions for wildcard pattern failures
403+
if len(wildcardPatterns) > 0 && len(allNames) == 0 {
404+
// All wildcard patterns failed AND no results from exact patterns - this is an error
405+
if len(failedPatterns) > 0 {
406+
return nil, fmt.Errorf("no models found: %v", failedPatterns)
407+
} else {
408+
return nil, fmt.Errorf("no models found")
409+
}
410+
} else if len(failedPatterns) > 0 {
411+
// Some patterns failed but we have results - log warning and continue with partial results
412+
glog.Warningf("Some wildcard patterns failed to expand and were skipped: %v", failedPatterns)
413+
}
414+
415+
return allNames, nil
416+
}
417+
363418
func (p *hfModelProvider) getModelsFromHF(ctx context.Context) ([]ModelProviderRecord, error) {
364-
var records []ModelProviderRecord
419+
// First expand any wildcard patterns to concrete model names
420+
expandedModels, err := p.expandModelNames(ctx, p.includedModels)
421+
if err != nil {
422+
return nil, fmt.Errorf("failed to expand model patterns: %w", err)
423+
}
365424

425+
var records []ModelProviderRecord
366426
currentTime := time.Now().UnixMilli()
367427
lastSyncedStr := strconv.FormatInt(currentTime, 10)
368428

369-
for _, modelName := range p.includedModels {
429+
for _, modelName := range expandedModels {
370430
// Skip if excluded - check before fetching to avoid unnecessary API calls
371431
if !p.filter.Allows(modelName) {
372432
glog.V(2).Infof("Skipping excluded model: %s", modelName)
@@ -726,6 +786,9 @@ func newHFModelProvider(ctx context.Context, source *Source, reldir string) (<-c
726786
p.baseURL = strings.TrimSuffix(url, "/")
727787
}
728788

789+
allowedOrg, _ := source.Properties[allowedOrgKey].(string)
790+
restrictToOrg(allowedOrg, &source.IncludedModels, &source.ExcludedModels)
791+
729792
// Parse sync interval (optional, defaults to 24 hours)
730793
// This can be configured as a duration string (e.g., "1s", "10s", "1m", "24h").
731794
// For testing, a shorter interval can be used to speed up tests.
@@ -805,6 +868,13 @@ func NewHFPreviewProvider(config *PreviewConfig) (*hfModelProvider, error) {
805868
p.baseURL = strings.TrimSuffix(url, "/")
806869
}
807870

871+
allowedOrg, _ := config.Properties[allowedOrgKey].(string)
872+
restrictToOrg(allowedOrg, &config.IncludedModels, &config.ExcludedModels)
873+
874+
if len(config.IncludedModels) == 0 {
875+
return nil, fmt.Errorf("includedModels is required for HuggingFace source preview (specifies which models to fetch from HuggingFace)")
876+
}
877+
808878
// Parse maxModels limit (optional, defaults to 500)
809879
// This limit is applied PER PATTERN (e.g., each "org/*" pattern gets its own limit)
810880
// to prevent overloading the Hugging Face API and respect rate limiting.
@@ -868,19 +938,27 @@ func parseModelPattern(pattern string) (PatternType, string, string) {
868938
return PatternOrgAll, org, ""
869939
}
870940

941+
parts := strings.SplitN(pattern, "/", 2)
942+
943+
org := parts[0]
944+
// Ensure org is not empty or a wildcard
945+
if org == "" || strings.Contains(org, "*") {
946+
return PatternInvalid, "", ""
947+
}
948+
949+
var model string
950+
if len(parts) == 2 {
951+
model = parts[1]
952+
if model == "" {
953+
return PatternInvalid, "", ""
954+
}
955+
}
956+
871957
// Check if it has a wildcard after org/prefix
872-
if strings.Contains(pattern, "/") && strings.HasSuffix(pattern, "*") {
873-
parts := strings.SplitN(pattern, "/", 2)
874-
if len(parts) == 2 {
875-
org := parts[0]
876-
// Ensure org is not empty or a wildcard
877-
if org == "" || org == "*" {
878-
return PatternInvalid, "", ""
879-
}
880-
prefix := strings.TrimSuffix(parts[1], "*")
881-
if prefix != "" {
882-
return PatternOrgPrefix, org, prefix
883-
}
958+
if strings.HasSuffix(model, "*") {
959+
prefix := strings.TrimSuffix(model, "*")
960+
if prefix != "" {
961+
return PatternOrgPrefix, org, prefix
884962
}
885963
}
886964

@@ -1085,3 +1163,29 @@ func (p *hfModelProvider) FetchModelNamesForPreview(ctx context.Context, modelId
10851163

10861164
return names, nil
10871165
}
1166+
1167+
// restrictToOrg prefixes included and excluded model lists with an
1168+
// organization name for convenience and to prevent any other organization from
1169+
// being retrieved.
1170+
func restrictToOrg(org string, included *[]string, excluded *[]string) {
1171+
if org == "" {
1172+
// No op
1173+
return
1174+
}
1175+
1176+
prefix := org + "/"
1177+
1178+
if included == nil || len(*included) == 0 {
1179+
*included = []string{prefix + "*"}
1180+
} else {
1181+
for i := range *included {
1182+
(*included)[i] = prefix + (*included)[i]
1183+
}
1184+
}
1185+
1186+
if excluded != nil {
1187+
for i := range *excluded {
1188+
(*excluded)[i] = prefix + (*excluded)[i]
1189+
}
1190+
}
1191+
}

0 commit comments

Comments
 (0)