Skip to content

Commit 9b8f4de

Browse files
Adds a simple lowest price model scorer (#71)
* Adds simple cost scorer Co-authored-by: Nir Rozenbaum <nir.rozenbaum@gmail.com> Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> Fixes Cost scorer to address reviewer comments Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> Remove legacy directories Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> Small refactoring to use better names Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> * Update pkg/framework/modelselector/scorer/costaware/plugin.go Co-authored-by: Nir Rozenbaum <nir.rozenbaum@gmail.com> Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> * Small refactoring after reimplementing Clone() for PriceValue struct Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> * Changes score implementation to inverted sum normalization. Changes the tests accordingly. Addresses reviewers' comments. Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> --------- Signed-off-by: David Breitgand <davidbreitgand@users.noreply.github.com> Co-authored-by: Nir Rozenbaum <nir.rozenbaum@gmail.com>
1 parent 900c74e commit 9b8f4de

2 files changed

Lines changed: 399 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
Copyright 2026 The llm-d Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package costaware
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
23+
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework"
24+
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/datalayer"
25+
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/modelselector"
26+
)
27+
28+
// Package costaware provides a scorer that scores candidate models based on expected cost
29+
// for an inference request, by ranking nominal prices of the models.
30+
// Model prices are expressed in USD per 1M tokens.
31+
// Each model in the model selector has a valid price.
32+
// The actual cost is calculated as the product of the number of tokens and the price per 1M tokens.
33+
// This scorer assumes that there are no price reversals and the lowest nominal price of a model
34+
// results in the lowest cost for the request.
35+
36+
const (
37+
// CostScorerType is the type of the CostScorer scorer
38+
CostScorerType = "cost-scorer"
39+
40+
// PriceAttributeKey is the key used to retrieve the price from model attributes
41+
PriceAttributeKey = "price"
42+
)
43+
44+
// PriceValue is a Cloneable wrapper for float64 price values
45+
type PriceValue struct {
46+
Value float64
47+
}
48+
49+
// Clone implements the Cloneable interface
50+
func (p *PriceValue) Clone() datalayer.Cloneable {
51+
return &PriceValue{Value: p.Value}
52+
}
53+
54+
// compile-time type assertion
55+
var _ modelselector.Scorer = &CostScorer{}
56+
57+
// CostScorerFactory defines the factory function for the CostScorer scorer
58+
func CostScorerFactory(name string, _ json.RawMessage, _ framework.Handle) (framework.Plugin, error) {
59+
return NewCostScorer().WithName(name), nil
60+
}
61+
62+
// NewCostScorer creates a new lowest price scorer
63+
func NewCostScorer() *CostScorer {
64+
return &CostScorer{
65+
typedName: framework.TypedName{Type: CostScorerType},
66+
}
67+
}
68+
69+
// CostScorer scorer that scores models based on their price
70+
// Lower-priced models receive higher scores
71+
type CostScorer struct {
72+
typedName framework.TypedName
73+
}
74+
75+
// TypedName returns the typed name of the plugin.
76+
func (s *CostScorer) TypedName() framework.TypedName {
77+
return s.typedName
78+
}
79+
80+
// WithName sets the name of the plugin.
81+
func (s *CostScorer) WithName(name string) *CostScorer {
82+
s.typedName.Name = name
83+
return s
84+
}
85+
86+
// Score scores the given models in range of [0.0-1.0] based on their price using inverted sum normalization.
87+
// Scoring behavior:
88+
// - Lower-priced models receive higher scores
89+
// - Score formula: 1.0 - price / sum(prices)
90+
// - Higher score indicates better (cheaper) model
91+
// - If only one model, it receives neutral score 0.5
92+
// - If all models have zero price, each receives score 1.0
93+
//
94+
// Note: When combining with other scorers using different normalization methods (e.g., Min-Max),
95+
// be aware that sum normalization may not preserve the intended weight proportions due to scale sensitivity.
96+
// For consistent multi-criteria scoring, consider using the same normalization method across all scorers.
97+
func (s *CostScorer) Score(_ context.Context, _ *framework.CycleState, _ *framework.InferenceRequest, models []datalayer.Model) map[datalayer.Model]float64 {
98+
// Create a map to hold the score of each model candidate
99+
scores := make(map[datalayer.Model]float64, len(models))
100+
101+
// Special case: single model gets neutral score
102+
if len(models) == 1 {
103+
scores[models[0]] = 0.5
104+
return scores
105+
}
106+
107+
// Calculate the sum of all prices
108+
var sumPrices float64
109+
for _, model := range models {
110+
priceValue, _ := model.GetAttributes().Get(PriceAttributeKey)
111+
price := priceValue.(*PriceValue).Value
112+
sumPrices += price
113+
}
114+
115+
// If sum is zero (all prices are zero), all models are free - assign perfect score
116+
if sumPrices == 0 {
117+
for _, model := range models {
118+
scores[model] = 1.0
119+
}
120+
return scores
121+
}
122+
123+
// Calculate scores using inverted sum normalization: 1 - price/sum(prices)
124+
for _, model := range models {
125+
priceValue, _ := model.GetAttributes().Get(PriceAttributeKey)
126+
price := priceValue.(*PriceValue).Value
127+
scores[model] = 1.0 - price/sumPrices
128+
}
129+
130+
return scores
131+
}

0 commit comments

Comments
 (0)