Skip to content

Commit 9f5d392

Browse files
committed
fix: broaden model recommendation candidates
1 parent 8be9018 commit 9f5d392

7 files changed

Lines changed: 415 additions & 39 deletions

File tree

internal/adminapi/handlers.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,7 @@ func (s *Server) buildIndexData(r *http.Request) indexData {
393393
// Knee-point value leader: best quality/price among frontier models
394394
// with quality ≥ 50% of top. Skipped when the top model is already
395395
// the value leader.
396-
axes := p.Axes
397-
if axes == nil && preset == "classifier" {
398-
axes = classifierAxes(models)
399-
}
396+
axes := axesForPreset(preset, p, models)
400397
if axes != nil {
401398
if vl := valueLeader(models, axes, 0.5); vl != nil {
402399
topQ, topP := axes(models[0])
@@ -599,6 +596,9 @@ func attachModelTelemetry(models []uiModel, telemetry map[string]modelTelemetry)
599596
// AAModelInfo record. Used by both the preset path and the browse path so
600597
// they share one formula for Think time and Markup %.
601598
func enrichFromAA(m *uiModel, aa llm.AAModelInfo) {
599+
if aa.Score > 0 {
600+
m.Score = aa.Score
601+
}
602602
m.CodingIndex = aa.CodingIndex
603603
m.AgenticIndex = aa.AgenticIndex
604604
m.SpeedTPS = aa.SpeedTPS

internal/adminapi/recommend.go

Lines changed: 158 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,26 @@ var specialisedCoderRegex = regexp.MustCompile(`-coder(-|$|:)`)
3535

3636
var specialisedVisionRegex = regexp.MustCompile(`-vl-`)
3737

38+
var unstableVariantRegex = regexp.MustCompile(
39+
`(^|[-/:])(preview|beta|exp|experimental|customtools)([-/:]|$)`,
40+
)
41+
42+
// lightweightDefaultRegex catches models that are good L1/classifier
43+
// candidates but too small or latency-oriented for the default workhorse role.
44+
// Do not match every "flash" model: Gemini Flash-class models can still be a
45+
// reasonable default, while "flash-lite"/"mini"/small-B variants usually are
46+
// not.
47+
var lightweightDefaultRegex = regexp.MustCompile(
48+
`(^|[-/:])(flash-lite|lite|mini|small|nano)([-/:]|$)|` +
49+
`(^|[-/:])([1-9]|1[0-4])b([-/:]|$)|` +
50+
`^qwen/qwen3(\.[0-9]+)?-flash([-/:]|$)`,
51+
)
52+
3853
// thinkingRegex matches model ids that are actually frontier reasoners — the
3954
// `reasoning: true` capability flag alone is not enough (8B models also set
4055
// it just because the API accepts a reasoning parameter).
4156
var thinkingRegex = regexp.MustCompile(
42-
`(-thinking|:thinking|/qwq|/deepseek-r[0-9]|-r1(-|$)|-reasoner)`,
57+
`(-thinking|:thinking|/qwq|/deepseek-r[0-9]|-r1(-|$)|-reasoner|^x-ai/grok-(3-mini|4))`,
4358
)
4459

4560
// paretoAxes returns (quality, price) for a model under a given role.
@@ -79,6 +94,10 @@ func effectivePromptOf(m uiModel) float64 {
7994
return m.PromptPrice
8095
}
8196

97+
func effectiveBlendedPriceOf(m uiModel) float64 {
98+
return orBlendedPrice(effectivePromptOf(m), m.CompletionPrice)
99+
}
100+
82101
// usesVisionFallback returns true for roles where non-vision traffic routes
83102
// image messages to the multimodal slot (so the role's candidates should be
84103
// penalised for missing vision).
@@ -102,62 +121,68 @@ func inverseTTFT(m uiModel) float64 {
102121

103122
var rolePresets = map[string]rolePreset{
104123
"simple": {
105-
Description: "tools + multilingual, ≤ $0.2/M prompt, ctx ≥ 32k. Pareto frontier on (AA Agentic Index, prompt price).",
124+
Description: "tools + multilingual, ≤ $0.2/M prompt, ctx ≥ 32k. Pareto frontier on (1/TTFT, blended cost) when speed data is available, else (AA Agentic Index, blended cost).",
106125
Filter: func(c llm.Capabilities, id string, aa llm.AAModelInfo) bool {
107126
return multilingualRegex.MatchString(id) &&
108127
!excludedVendorsRegex.MatchString(id) &&
109128
!specialisedCoderRegex.MatchString(id) &&
110129
!specialisedVisionRegex.MatchString(id) &&
130+
!isUnstableVariant(id) &&
111131
!isFreeVariant(id) &&
112132
c.Tools &&
113133
c.ContextLength >= 32000 &&
114134
c.PromptPrice > 0 && c.PromptPrice <= 0.2
115135
},
116-
Axes: func(m uiModel) (float64, float64) { return bestAgentic(m), effectivePromptOf(m) },
136+
// Axes selected dynamically in applyPreset (see simpleAxes).
137+
Axes: nil,
117138
},
118139

119140
"default": {
120-
Description: "tools + multilingual, ≤ $2/M prompt, ctx ≥ 32k. Pareto frontier on (AA Agentic Index, prompt price).",
141+
Description: "workhorse tools + multilingual, excludes L1/classifier-sized models, ≤ $2/M prompt, ctx ≥ 32k. Pareto frontier on (AA Agentic Index, blended cost).",
121142
Filter: func(c llm.Capabilities, id string, aa llm.AAModelInfo) bool {
122143
return multilingualRegex.MatchString(id) &&
123144
!excludedVendorsRegex.MatchString(id) &&
124145
!specialisedCoderRegex.MatchString(id) &&
125146
!specialisedVisionRegex.MatchString(id) &&
147+
!lightweightDefaultRegex.MatchString(id) &&
148+
!isUnstableVariant(id) &&
126149
!isFreeVariant(id) &&
127150
c.Tools &&
128151
c.ContextLength >= 32000 &&
129152
c.PromptPrice > 0 && c.PromptPrice <= 2.0
130153
},
131-
Axes: func(m uiModel) (float64, float64) { return bestAgentic(m), effectivePromptOf(m) },
154+
Axes: func(m uiModel) (float64, float64) { return bestAgentic(m), effectiveBlendedPriceOf(m) },
132155
},
133156

134157
"complex": {
135-
Description: "frontier reasoners (thinking/r1/qwq) with tools + multilingual, ≤ $5/M prompt, ctx ≥ 64k. Pareto frontier on (AA Agentic Index, prompt price). Claude via bridge is preferred when configured.",
158+
Description: "frontier reasoners (thinking/r1/qwq/grok) with tools + multilingual, ≤ $5/M prompt, ctx ≥ 64k. Pareto frontier on (AA Agentic Index, blended cost). Claude via bridge is preferred when configured.",
136159
Filter: func(c llm.Capabilities, id string, aa llm.AAModelInfo) bool {
137160
return multilingualRegex.MatchString(id) &&
138161
!excludedVendorsRegex.MatchString(id) &&
139162
!specialisedCoderRegex.MatchString(id) &&
140163
!specialisedVisionRegex.MatchString(id) &&
164+
!isUnstableVariant(id) &&
141165
!isFreeVariant(id) &&
142166
thinkingRegex.MatchString(id) &&
143167
c.Tools && c.Reasoning &&
144168
c.ContextLength >= 64000 &&
145169
c.PromptPrice > 0 && c.PromptPrice <= 5.0
146170
},
147-
Axes: func(m uiModel) (float64, float64) { return bestAgentic(m), effectivePromptOf(m) },
171+
Axes: func(m uiModel) (float64, float64) { return bestAgentic(m), effectiveBlendedPriceOf(m) },
148172
},
149173

150174
"multimodal": {
151-
Description: "vision + tools + multilingual, ≤ $2/M prompt, ctx ≥ 32k. Pareto frontier on (AA Intelligence Index, prompt price).",
175+
Description: "vision + tools + multilingual, ≤ $2/M prompt, ctx ≥ 32k. Pareto frontier on (AA Intelligence Index, blended cost).",
152176
Filter: func(c llm.Capabilities, id string, aa llm.AAModelInfo) bool {
153177
return multilingualRegex.MatchString(id) &&
154178
!excludedVendorsRegex.MatchString(id) &&
179+
!isUnstableVariant(id) &&
155180
!isFreeVariant(id) &&
156181
c.Vision && c.Tools &&
157182
c.ContextLength >= 32000 &&
158183
c.PromptPrice > 0 && c.PromptPrice <= 2.0
159184
},
160-
Axes: func(m uiModel) (float64, float64) { return m.Score, m.PromptPrice },
185+
Axes: func(m uiModel) (float64, float64) { return m.Score, effectiveBlendedPriceOf(m) },
161186
},
162187

163188
"compaction": {
@@ -167,6 +192,7 @@ var rolePresets = map[string]rolePreset{
167192
!excludedVendorsRegex.MatchString(id) &&
168193
!specialisedCoderRegex.MatchString(id) &&
169194
!specialisedVisionRegex.MatchString(id) &&
195+
!isUnstableVariant(id) &&
170196
!isFreeVariant(id) &&
171197
c.ContextLength >= 64000 &&
172198
c.CompletionPrice > 0 && c.CompletionPrice <= 2.0
@@ -181,6 +207,7 @@ var rolePresets = map[string]rolePreset{
181207
!excludedVendorsRegex.MatchString(id) &&
182208
!specialisedCoderRegex.MatchString(id) &&
183209
!specialisedVisionRegex.MatchString(id) &&
210+
!isUnstableVariant(id) &&
184211
!isFreeVariant(id) &&
185212
c.PromptPrice > 0 && c.PromptPrice <= 0.1
186213
},
@@ -196,6 +223,10 @@ func isFreeVariant(modelID string) bool {
196223
return len(modelID) > 5 && modelID[len(modelID)-5:] == ":free"
197224
}
198225

226+
func isUnstableVariant(modelID string) bool {
227+
return unstableVariantRegex.MatchString(modelID)
228+
}
229+
199230
// classifierAxes picks (1/TTFT, price) when at least one candidate has TTFT
200231
// data, otherwise falls back to (Score, price). Mixing two quality scales in
201232
// the same Pareto frontier would be meaningless, so we pick one globally.
@@ -208,6 +239,32 @@ func classifierAxes(candidates []uiModel) paretoAxes {
208239
return func(m uiModel) (float64, float64) { return m.Score, m.PromptPrice }
209240
}
210241

242+
// simpleAxes keeps L1 recommendations distinct from default: when AA speed
243+
// data exists, startup latency is the quality axis. Without TTFT data, fall
244+
// back to agentic quality so new/untested models are not promoted blindly.
245+
func simpleAxes(candidates []uiModel) paretoAxes {
246+
for _, m := range candidates {
247+
if m.TTFT > 0 {
248+
return func(m uiModel) (float64, float64) { return inverseTTFT(m), effectiveBlendedPriceOf(m) }
249+
}
250+
}
251+
return func(m uiModel) (float64, float64) { return bestAgentic(m), effectiveBlendedPriceOf(m) }
252+
}
253+
254+
func axesForPreset(role string, preset rolePreset, candidates []uiModel) paretoAxes {
255+
if preset.Axes != nil {
256+
return preset.Axes
257+
}
258+
switch role {
259+
case "simple":
260+
return simpleAxes(candidates)
261+
case "classifier":
262+
return classifierAxes(candidates)
263+
default:
264+
return nil
265+
}
266+
}
267+
211268
// paretoFrontier keeps only non-dominated models. A model is also excluded
212269
// if its quality is 0 — Pareto would otherwise keep untested models at the
213270
// price floor just because no one beats them on both axes. Requiring quality
@@ -241,6 +298,94 @@ func paretoFrontier(models []uiModel, axes paretoAxes) []uiModel {
241298
return out
242299
}
243300

301+
func appendNearFrontierAlternatives(frontier, candidates []uiModel, axes paretoAxes, role string, limit int) []uiModel {
302+
if len(candidates) == 0 || axes == nil || limit <= 0 {
303+
return frontier
304+
}
305+
seen := make(map[string]bool, len(frontier))
306+
topQuality := 0.0
307+
for _, m := range candidates {
308+
q, _ := axes(m)
309+
if q > topQuality {
310+
topQuality = q
311+
}
312+
}
313+
if topQuality <= 0 {
314+
return frontier
315+
}
316+
for _, m := range frontier {
317+
seen[m.ID] = true
318+
}
319+
alts := make([]uiModel, 0, len(candidates))
320+
for _, m := range candidates {
321+
if seen[m.ID] {
322+
continue
323+
}
324+
q, p := axes(m)
325+
if q <= 0 || p <= 0 || q < 0.50*topQuality {
326+
continue
327+
}
328+
m.Recommended = false
329+
if p > 0 {
330+
m.ValuePerDollar = q / p
331+
}
332+
annotateModelForRole(&m, role, "near_frontier")
333+
alts = append(alts, m)
334+
}
335+
sort.Slice(alts, func(i, j int) bool {
336+
qi, pi := axes(alts[i])
337+
qj, pj := axes(alts[j])
338+
if qi != qj {
339+
return qi > qj
340+
}
341+
if pi != pj {
342+
return pi < pj
343+
}
344+
return alts[i].ID < alts[j].ID
345+
})
346+
if len(alts) > limit {
347+
alts = alts[:limit]
348+
}
349+
out := append(frontier, alts...)
350+
return appendUntestedAlternatives(out, candidates, axes, role, 2)
351+
}
352+
353+
func appendUntestedAlternatives(models, candidates []uiModel, axes paretoAxes, role string, limit int) []uiModel {
354+
if limit <= 0 {
355+
return models
356+
}
357+
seen := make(map[string]bool, len(models))
358+
for _, m := range models {
359+
seen[m.ID] = true
360+
}
361+
untested := make([]uiModel, 0, len(candidates))
362+
for _, m := range candidates {
363+
if seen[m.ID] {
364+
continue
365+
}
366+
q, _ := axes(m)
367+
if q > 0 {
368+
continue
369+
}
370+
m.Recommended = false
371+
annotateModelForRole(&m, role, "untested")
372+
untested = append(untested, m)
373+
}
374+
sort.Slice(untested, func(i, j int) bool {
375+
if untested[i].ContextLength != untested[j].ContextLength {
376+
return untested[i].ContextLength > untested[j].ContextLength
377+
}
378+
if untested[i].PromptPrice != untested[j].PromptPrice {
379+
return untested[i].PromptPrice < untested[j].PromptPrice
380+
}
381+
return untested[i].ID < untested[j].ID
382+
})
383+
if len(untested) > limit {
384+
untested = untested[:limit]
385+
}
386+
return append(models, untested...)
387+
}
388+
244389
// applyPreset returns the Pareto-optimal models for the role, sorted by
245390
// quality descending (best first). If the role has no preset, returns nil.
246391
// Each returned model has ValuePerDollar populated using the role's axes.
@@ -283,9 +428,9 @@ func applyPreset(all map[string]llm.Capabilities, aaModels map[string]llm.AAMode
283428
}
284429
candidates = append(candidates, m)
285430
}
286-
axes := preset.Axes
287-
if axes == nil && role == "classifier" {
288-
axes = classifierAxes(candidates)
431+
axes := axesForPreset(role, preset, candidates)
432+
if axes == nil {
433+
return nil
289434
}
290435
frontier := paretoFrontier(candidates, axes)
291436
sort.Slice(frontier, func(i, j int) bool {
@@ -307,7 +452,7 @@ func applyPreset(all map[string]llm.Capabilities, aaModels map[string]llm.AAMode
307452
frontier[i].Recommended = true
308453
annotateModelForRole(&frontier[i], role, "preset")
309454
}
310-
return frontier
455+
return appendNearFrontierAlternatives(frontier, candidates, axes, role, 4)
311456
}
312457

313458
func annotateModelForRole(m *uiModel, role, source string) {

internal/adminapi/recommend_inspection_test.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,10 @@ func TestPresetInspection(t *testing.T) {
5151
results := applyPreset(caps, cache.Models, role, visionFallbackPrompt)
5252

5353
var lines []string
54-
lines = append(lines, fmt.Sprintf("\n=== %s (%d candidates on Pareto frontier) ===", role, len(results)))
54+
lines = append(lines, fmt.Sprintf("\n=== %s (%d preset results: recommended + alternatives) ===", role, len(results)))
5555
lines = append(lines, preset.Description)
5656

57-
axes := preset.Axes
58-
if axes == nil && role == "classifier" {
59-
axes = classifierAxes(results)
60-
}
57+
axes := axesForPreset(role, preset, results)
6158
if vl := valueLeader(results, axes, 0.5); vl != nil {
6259
topQ, topP := axes(results[0])
6360
vQ, vP := axes(*vl)

0 commit comments

Comments
 (0)