-
Notifications
You must be signed in to change notification settings - Fork 44
feat: Extend model name Filter-189 #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
aviavissar
wants to merge
1
commit into
llm-d:main
from
aviavissar:feat-Extend-model-name-Filter-#189
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
pkg/framework/plugins/modelselector/filter/byfieldattribute/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # By-Field Filter (and Model Name Filter) | ||
|
|
||
| Restricts the candidate models based on a configurable field in the request body, compared | ||
| against either the candidate model's name or one of its attributes. | ||
|
|
||
| The filter matches the requested value against the candidate models the pipeline hands it from | ||
| the datalayer. "Available" below means "present in that candidate list". | ||
|
|
||
| ## `by-field-filter` | ||
|
|
||
| The generic filter. Registered as type `by-field-filter` and runs as a modelselector filter. | ||
|
|
||
| ## What it does | ||
|
|
||
| 1. Reads the configured `fieldName` field from the request body. | ||
| 2. Compares that value against each candidate's comparison value: | ||
| - By default, the comparison value is the candidate's name (`Model.GetName()`). | ||
| - When `byField` is set, the value compares with the `byField` value attribute for | ||
| the model, which is defined in the candidate's `AttributeMap`. | ||
| 3. Returns every candidate whose comparison value matches. Candidates that don't carry the | ||
| `byField` attribute (when configured) are skipped, not treated as an error. | ||
| 4. If the field is absent or an empty string, all incoming candidates are kept. | ||
| 5. If no candidate matches, or the field is malformed (not a string), the filter returns no | ||
| candidates and the pipeline rejects the request with HTTP 429. | ||
|
|
||
| ## Configuration | ||
|
|
||
| - `fieldName` (required) — the request-body field to read the comparison value from. | ||
| - `byField` (optional) — the name of the candidate attribute to compare against. | ||
| When empty or omitted, defaults to the candidate's name (`Model.GetName()`). | ||
|
|
||
| ## Inputs consumed | ||
|
|
||
| - The `fieldName` field of the request body. | ||
| - The candidate model list passed in by the pipeline, including each candidate's attributes (when | ||
| `byField` is configured). | ||
|
|
||
| ## Example configuration: filter by model name | ||
|
|
||
| Equivalent to the built-in `model-name-filter` (see below): | ||
|
|
||
| ```yaml | ||
| plugins: | ||
| - type: by-field-filter | ||
| params: | ||
| fieldName: model | ||
| ``` | ||
|
|
||
| ## Example configuration: filter by a custom field/attribute | ||
|
|
||
| For example, filtering Anthropic-on-Bedrock requests by their `anthropic_version` field, matched | ||
| against a candidate attribute named `anthropicVersion` (populated by another plugin, e.g. the | ||
| `model-config-datasource` plugin, onto each candidate's `AttributeMap`): | ||
|
|
||
| ```yaml | ||
| plugins: | ||
| - type: by-field-filter | ||
| params: | ||
| fieldName: anthropic_version | ||
| byField: anthropicVersion | ||
| ``` | ||
|
|
||
| ## `model-name-filter` | ||
|
|
||
| Kept as a separate, backward-compatible plugin type with the original, fixed behavior: it always | ||
| reads the `model` field and compares by the candidate's name, ignoring any parameters passed to it. | ||
|
|
||
| It is registered as type `model-name-filter` and behaves exactly like `by-field-filter` configured | ||
| with `fieldName: model` and no `byField` override. | ||
|
|
||
| ```yaml | ||
| plugins: | ||
| - type: model-name-filter | ||
| ``` |
167 changes: 167 additions & 0 deletions
167
pkg/framework/plugins/modelselector/filter/byfieldattribute/filter.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| /* | ||
| Copyright 2026 The llm-d Authors. | ||
|
|
||
| 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. | ||
| */ | ||
|
|
||
| // Package byfieldattribute implements a modelselector filter that matches | ||
| // candidates by name or attribute. See the package README for details. | ||
| package byfieldattribute | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "sigs.k8s.io/controller-runtime/pkg/log" | ||
|
|
||
| logutil "github.com/llm-d/llm-d-inference-payload-processor/pkg/common/observability/logging" | ||
| "github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/datalayer" | ||
| "github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/modelselector" | ||
| "github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/plugin" | ||
| "github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/requesthandling" | ||
| ) | ||
|
|
||
| type StringAttribute = datalayer.StringAttribute | ||
|
|
||
| const ( | ||
| // ModelNameFilterType is the registered plugin type for model-name-filter. | ||
| ModelNameFilterType = "model-name-filter" | ||
| // ByFieldFilterType is the registered plugin type for by-field-filter. | ||
| ByFieldFilterType = "by-field-filter" | ||
|
|
||
| requestModelField = "model" | ||
| defaultByFieldValue = "" | ||
| ) | ||
|
|
||
| var _ modelselector.Filter = &ByFieldFilter{} | ||
| var _ datalayer.Cloneable = StringAttribute("") | ||
|
|
||
| // ByFieldFilterConfig is the JSON parameter shape for ByFieldFilter. | ||
| type ByFieldFilterConfig struct { | ||
| FieldName string `json:"fieldName"` | ||
| ByField string `json:"byField,omitempty"` | ||
| } | ||
|
|
||
| // ModelNameFilterFactory ignores its parameters: fieldName is always "model" | ||
| // and byField always defaults to Model.GetName(). | ||
| func ModelNameFilterFactory(name string, _ json.RawMessage, _ plugin.Handle) (plugin.Plugin, error) { | ||
| return newByFieldFilter(ModelNameFilterType, requestModelField, defaultByFieldValue).WithName(name), nil | ||
| } | ||
|
|
||
| // ByFieldFilterFactory parses fieldName (required) and byField (optional) | ||
| // from params and returns a configured ByFieldFilter. | ||
| func ByFieldFilterFactory(name string, params json.RawMessage, _ plugin.Handle) (plugin.Plugin, error) { | ||
| var cfg ByFieldFilterConfig | ||
| if len(params) > 0 { | ||
| if err := json.Unmarshal(params, &cfg); err != nil { | ||
| return nil, fmt.Errorf("failed to parse parameters of '%s': %w", ByFieldFilterType, err) | ||
| } | ||
| } | ||
| if cfg.FieldName == "" { | ||
| return nil, fmt.Errorf("'%s' requires a non-empty fieldName parameter", ByFieldFilterType) | ||
| } | ||
|
|
||
| return newByFieldFilter(ByFieldFilterType, cfg.FieldName, cfg.ByField).WithName(name), nil | ||
| } | ||
|
|
||
| // NewModelNameFilter initializes a ByFieldFilter with model-name-filter's | ||
| // fixed settings (fieldName "model", default byField). | ||
| func NewModelNameFilter() *ByFieldFilter { | ||
| return newByFieldFilter(ModelNameFilterType, requestModelField, defaultByFieldValue) | ||
| } | ||
|
|
||
| // NewByFieldFilter initializes a by-field-filter with the given fieldName | ||
| // and byField (empty byField means Model.GetName()). | ||
| func NewByFieldFilter(fieldName, byField string) *ByFieldFilter { | ||
| return newByFieldFilter(ByFieldFilterType, fieldName, byField) | ||
| } | ||
|
|
||
| func newByFieldFilter(typ, fieldName, byField string) *ByFieldFilter { | ||
| if typ == ModelNameFilterType { | ||
| log.Log.Info("'" + ModelNameFilterType + "' is deprecated and will be removed in ipp v0.3.0; " + | ||
| "use '" + ByFieldFilterType + "' with fieldName: model instead") | ||
| } | ||
| return &ByFieldFilter{ | ||
| typedName: plugin.TypedName{Type: typ, Name: typ}, | ||
| fieldName: fieldName, | ||
| byField: byField, | ||
| } | ||
| } | ||
|
|
||
| // ByFieldFilter restricts candidates to those whose comparison value (the | ||
| // name, or a configured attribute) matches a configurable request body field. | ||
| type ByFieldFilter struct { | ||
| typedName plugin.TypedName | ||
| fieldName string | ||
| byField string | ||
| } | ||
|
|
||
| func (f *ByFieldFilter) TypedName() plugin.TypedName { | ||
| return f.typedName | ||
| } | ||
|
|
||
| func (f *ByFieldFilter) WithName(name string) *ByFieldFilter { | ||
| f.typedName.Name = name | ||
| return f | ||
| } | ||
|
|
||
| // Filter keeps candidates whose comparison value matches the request field. | ||
| // An absent or empty field keeps all candidates; a non-string field value is | ||
| // malformed and yields none (the pipeline rejects the request). | ||
| func (f *ByFieldFilter) Filter(ctx context.Context, _ *plugin.CycleState, request *requesthandling.InferenceRequest, models []datalayer.Model) []datalayer.Model { | ||
| logger := log.FromContext(ctx) | ||
|
|
||
| raw := request.Body[f.fieldName] | ||
| requested, ok := raw.(string) | ||
| if !ok && raw != nil { | ||
| logger.V(logutil.VERBOSE).Info("malformed request field, no available model candidates", "field", f.fieldName) | ||
| return []datalayer.Model{} | ||
| } | ||
| if requested == "" { | ||
| logger.V(logutil.VERBOSE).Info("no value in request field. All available models are considered as candidates", "field", f.fieldName) | ||
| return models | ||
| } | ||
|
|
||
| result := make([]datalayer.Model, 0, len(models)) | ||
| for _, model := range models { | ||
| value, found := f.candidateValue(model) | ||
| if !found { | ||
| continue | ||
| } | ||
| if value == requested { | ||
| result = append(result, model) | ||
| } | ||
| } | ||
|
|
||
| if len(result) == 0 { | ||
| logger.V(logutil.VERBOSE).Info("request field value is not configured", "requested", requested) | ||
| return []datalayer.Model{} | ||
| } | ||
|
|
||
| logger.V(logutil.DEBUG).Info("by-field filter applied", "requested", requested, "candidates", len(result)) | ||
| return result | ||
| } | ||
|
|
||
| // candidateValue returns the model's name by default, or its byField | ||
| // attribute when configured; false if that attribute is missing. | ||
| func (f *ByFieldFilter) candidateValue(model datalayer.Model) (string, bool) { | ||
| if f.byField == defaultByFieldValue { | ||
| return model.GetName(), true | ||
| } | ||
| value, err := datalayer.ReadAttributeKey[StringAttribute](model.GetAttributes(), f.byField) | ||
| if err != nil { | ||
| return "", false | ||
| } | ||
| return string(value), true | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.