Skip to content

Commit 09a3ce2

Browse files
committed
Explain model recommendation badges
1 parent 1e44740 commit 09a3ce2

7 files changed

Lines changed: 299 additions & 95 deletions

File tree

internal/adminapi/adminapi_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,12 +418,21 @@ func TestTGAdminRecommendedModelsAppendFreeCandidatesUnrecommended(t *testing.T)
418418
for _, model := range payload.Models {
419419
if model.Recommended && !model.Free {
420420
sawRecommended = true
421+
if model.PrimaryReason != "Pareto frontier for simple" {
422+
t.Fatalf("recommended primary reason = %q, want role-specific Pareto explanation: %+v", model.PrimaryReason, model)
423+
}
424+
if strings.EqualFold(model.PolicyLabel, "recommended") {
425+
t.Fatalf("recommended policy label should be suppressed, got %+v", model)
426+
}
421427
}
422428
if model.Free {
423429
sawFreeCandidate = true
424430
if model.Recommended || model.Policy != "free_unverified" || model.Source != "free" {
425431
t.Fatalf("unexpected free candidate flags: %+v", model)
426432
}
433+
if model.StatusLabel != "Untested" || model.PolicyLabel != "Free unverified" {
434+
t.Fatalf("unexpected free candidate display labels: %+v", model)
435+
}
427436
}
428437
}
429438
if !sawRecommended || !sawFreeCandidate {

internal/adminapi/handlers.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,37 @@ type uiSlot struct {
3131
}
3232

3333
type uiModel struct {
34-
ID string
35-
PromptPrice float64
36-
CompletionPrice float64
37-
ContextLength int
38-
Vision bool
39-
Tools bool
40-
Reasoning bool
41-
Free bool
42-
Score float64 // AA Intelligence Index
43-
CodingIndex float64 // AA Coding Index
44-
AgenticIndex float64 // AA Agentic Index
45-
SpeedTPS float64 // median output tokens/sec
46-
TTFT float64 // median time-to-first-token, seconds
47-
ThinkTime float64 // TTFA - TTFT — time spent thinking before answer starts (reasoners only)
48-
AAPriceBlended float64 // AA's reference blended 3:1 input/output price (USD / 1M)
49-
MarkupPct float64 // (OR blended - AA blended) / AA blended × 100 — positive means OR charges more
50-
EffectivePrompt float64 // 0.9 × prompt + 0.1 × multimodal_slot.prompt for non-vision candidates under roles that route images elsewhere
51-
ValuePerDollar float64 // quality / prompt price (role-specific in preset path; agent/$ in browse path)
52-
Recommended bool
53-
Source string
54-
Policy string
55-
Section string
56-
OverrideNote string
57-
Reasons []string
58-
Warnings []string
59-
Telemetry modelTelemetry
60-
Market llm.OpenRouterMarketSignal
34+
ID string
35+
PromptPrice float64
36+
CompletionPrice float64
37+
ContextLength int
38+
Vision bool
39+
Tools bool
40+
Reasoning bool
41+
Free bool
42+
Score float64 // AA Intelligence Index
43+
CodingIndex float64 // AA Coding Index
44+
AgenticIndex float64 // AA Agentic Index
45+
SpeedTPS float64 // median output tokens/sec
46+
TTFT float64 // median time-to-first-token, seconds
47+
ThinkTime float64 // TTFA - TTFT — time spent thinking before answer starts (reasoners only)
48+
AAPriceBlended float64 // AA's reference blended 3:1 input/output price (USD / 1M)
49+
MarkupPct float64 // (OR blended - AA blended) / AA blended × 100 — positive means OR charges more
50+
EffectivePrompt float64 // 0.9 × prompt + 0.1 × multimodal_slot.prompt for non-vision candidates under roles that route images elsewhere
51+
ValuePerDollar float64 // quality / prompt price (role-specific in preset path; agent/$ in browse path)
52+
Recommended bool
53+
Source string
54+
Policy string
55+
Section string
56+
OverrideNote string
57+
Reasons []string
58+
Warnings []string
59+
Telemetry modelTelemetry
60+
Market llm.OpenRouterMarketSignal
61+
StatusLabel string
62+
PolicyLabel string
63+
PrimaryReason string
64+
SecondaryReasons []string
6165
}
6266

6367
type modelTelemetry struct {
@@ -405,6 +409,7 @@ func (s *Server) buildIndexData(r *http.Request) indexData {
405409
models = filterModelOverrides(models, catalogProv, overrides, true)
406410
attachMarketSignals(models, marketSignals)
407411
attachModelTelemetry(models, s.loadModelTelemetry(ctx5, catalogProv))
412+
decorateModelDisplay(models, preset)
408413
sections := buildModelSections(models, true)
409414
filters := uiFilters{
410415
ActivePreset: preset,
@@ -516,6 +521,7 @@ func (s *Server) buildIndexData(r *http.Request) indexData {
516521
models = filterModelOverrides(models, catalogProv, overrides, f.Search == "")
517522
attachMarketSignals(models, marketSignals)
518523
attachModelTelemetry(models, s.loadModelTelemetry(ctx5, catalogProv))
524+
decorateModelDisplay(models, "")
519525
asc := sortDir == "asc"
520526
sort.Slice(models, func(i, j int) bool {
521527
var less bool

internal/adminapi/model_display.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package adminapi
2+
3+
import "strings"
4+
5+
type modelDisplay struct {
6+
StatusLabel string
7+
PolicyLabel string
8+
PrimaryReason string
9+
SecondaryReasons []string
10+
}
11+
12+
func decorateModelDisplay(models []uiModel, role string) {
13+
for i := range models {
14+
applyModelDisplay(&models[i], role, false)
15+
}
16+
}
17+
18+
func applyModelDisplay(m *uiModel, role string, current bool) {
19+
d := modelDisplayFor(*m, role, current)
20+
m.StatusLabel = d.StatusLabel
21+
m.PolicyLabel = d.PolicyLabel
22+
m.PrimaryReason = d.PrimaryReason
23+
m.SecondaryReasons = d.SecondaryReasons
24+
}
25+
26+
func modelDisplayFor(m uiModel, role string, current bool) modelDisplay {
27+
status := modelStatusLabel(m, current)
28+
policy := modelPolicyLabel(m, status)
29+
primary := modelPrimaryReason(m, role, status)
30+
secondary := modelSecondaryReasons(m.Reasons, primary)
31+
return modelDisplay{
32+
StatusLabel: status,
33+
PolicyLabel: policy,
34+
PrimaryReason: primary,
35+
SecondaryReasons: secondary,
36+
}
37+
}
38+
39+
func modelStatusLabel(m uiModel, current bool) string {
40+
if current {
41+
return "Current"
42+
}
43+
switch modelSectionKey(m) {
44+
case "recommended":
45+
return "Recommended"
46+
case "interesting":
47+
return "Interesting"
48+
case "untested":
49+
return "Untested"
50+
case "blocked":
51+
return "Blocked"
52+
default:
53+
return "Candidate"
54+
}
55+
}
56+
57+
func modelPolicyLabel(m uiModel, status string) string {
58+
switch m.Policy {
59+
case "", "candidate", "recommended":
60+
return ""
61+
case "manual_allow":
62+
if status == "Interesting" {
63+
return "Manual allow"
64+
}
65+
return "Manual allow"
66+
case "manual_deny":
67+
if status == "Blocked" {
68+
return "Manual deny"
69+
}
70+
return "Manual deny"
71+
case "free_unverified":
72+
if status == "Untested" {
73+
return "Free unverified"
74+
}
75+
return "Free unverified"
76+
case "free_verified":
77+
return "Free verified"
78+
case "free_degraded":
79+
return "Free degraded"
80+
case "free_blocked":
81+
if status == "Blocked" {
82+
return "Free blocked"
83+
}
84+
return "Free blocked"
85+
default:
86+
label := strings.ReplaceAll(m.Policy, "_", " ")
87+
return strings.ToUpper(label[:1]) + label[1:]
88+
}
89+
}
90+
91+
func modelPrimaryReason(m uiModel, role, status string) string {
92+
switch {
93+
case m.Recommended:
94+
if role != "" {
95+
return "Pareto frontier for " + role
96+
}
97+
return "Pareto frontier"
98+
case m.Policy == "manual_allow":
99+
return firstNonEmpty("Manual allow", m.OverrideNote)
100+
case m.Policy == "manual_deny":
101+
return firstNonEmpty("Manual deny", m.OverrideNote)
102+
case m.Policy == "free_blocked":
103+
return "Free endpoint is blocked"
104+
case m.Policy == "free_degraded":
105+
return "Free endpoint degraded"
106+
case m.Policy == "free_verified":
107+
return "Free endpoint verified"
108+
case m.Policy == "free_unverified":
109+
return "Free endpoint needs a check"
110+
case m.Source == "near_frontier":
111+
if role != "" {
112+
return "Near frontier for " + role
113+
}
114+
return "Near recommendation frontier"
115+
case status == "Untested":
116+
return "Compatible but missing role evidence"
117+
}
118+
if len(m.Reasons) > 0 {
119+
return m.Reasons[0]
120+
}
121+
return ""
122+
}
123+
124+
func modelSecondaryReasons(reasons []string, primary string) []string {
125+
out := make([]string, 0, len(reasons))
126+
for _, reason := range uniqueStrings(reasons) {
127+
if reason == "" || reason == primary {
128+
continue
129+
}
130+
out = append(out, reason)
131+
}
132+
return out
133+
}
134+
135+
func firstNonEmpty(fallback, value string) string {
136+
if strings.TrimSpace(value) != "" {
137+
return fallback + ": " + strings.TrimSpace(value)
138+
}
139+
return fallback
140+
}

internal/adminapi/recommend_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,30 @@ func TestModelSectionsSplitRecommendationBuckets(t *testing.T) {
226226
}
227227
}
228228

229+
func TestModelDisplayDedupesRecommendedInternals(t *testing.T) {
230+
m := uiModel{
231+
ID: "deepseek/deepseek-v3.2",
232+
Recommended: true,
233+
Section: "recommended",
234+
Policy: "recommended",
235+
Reasons: []string{"tool calling", "AA coding 35", "ctx 128k"},
236+
}
237+
238+
got := modelDisplayFor(m, "default", false)
239+
if got.StatusLabel != "Recommended" {
240+
t.Fatalf("status = %q, want Recommended", got.StatusLabel)
241+
}
242+
if got.PolicyLabel != "" {
243+
t.Fatalf("policy label = %q, want empty duplicate-suppressed label", got.PolicyLabel)
244+
}
245+
if got.PrimaryReason != "Pareto frontier for default" {
246+
t.Fatalf("primary reason = %q", got.PrimaryReason)
247+
}
248+
if containsString(got.SecondaryReasons, "recommended") || containsString(got.SecondaryReasons, "Pareto frontier for default") {
249+
t.Fatalf("secondary reasons contain duplicated recommendation internals: %+v", got.SecondaryReasons)
250+
}
251+
}
252+
229253
func TestSectionDiversityCapsModelFamilies(t *testing.T) {
230254
models := []uiModel{
231255
{ID: "google/gemini-2.5-pro"},
@@ -247,6 +271,15 @@ func TestSectionDiversityCapsModelFamilies(t *testing.T) {
247271
}
248272
}
249273

274+
func containsString(values []string, want string) bool {
275+
for _, value := range values {
276+
if value == want {
277+
return true
278+
}
279+
}
280+
return false
281+
}
282+
250283
func TestDefaultPresetUsesCodingWhenAgenticIsMissing(t *testing.T) {
251284
caps := map[string]llm.Capabilities{
252285
"x-ai/grok-4.3": {

internal/adminapi/templates/partials_models_row.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
<td>
3434
<div class="model-id">{{.ID}}</div>
3535
<div style="margin-top:4px;color:var(--text-secondary);font-size:0.78rem;">
36-
{{if .Section}}<span class="badge badge--neutral">{{.Section}}</span>{{end}}
37-
{{if .Policy}}<span class="badge badge--neutral">{{.Policy}}</span>{{end}}
38-
{{range .Reasons}}<span>{{.}}</span> {{end}}
36+
{{if .StatusLabel}}<span class="badge badge--neutral">{{.StatusLabel}}</span>{{end}}
37+
{{if .PolicyLabel}}<span class="badge badge--neutral">{{.PolicyLabel}}</span>{{end}}
38+
{{if .PrimaryReason}}<span>Why: {{.PrimaryReason}}</span>{{end}}
39+
{{range .SecondaryReasons}}<span>{{.}}</span> {{end}}
3940
</div>
4041
{{if .Warnings}}
4142
<div style="margin-top:4px;color:var(--warning, #9a5b00);font-size:0.78rem;">

internal/adminapi/templates/tg_admin.html

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -586,17 +586,17 @@ <h2 id="sheet-title">Choose model</h2>
586586
var chips = document.createElement("div");
587587
chips.className = "chips";
588588
[
589-
model.current ? "current" : "",
590-
model.recommended ? "recommended" : "",
591-
model.section || "",
592-
model.policy || "",
589+
model.status_label || (model.current ? "Current" : ""),
590+
model.policy_label || "",
593591
model.tools ? "tools" : "",
594592
model.vision ? "vision" : "",
595593
model.reasoning ? "reasoning" : "",
596594
model.market && model.market.rank ? "market #" + model.market.rank : "",
597595
model.market && !model.market.rank && (model.market.share || model.market.score) ? "market" : "",
598596
model.agentic_index ? "agentic " + Math.round(model.agentic_index) : ""
599-
].filter(Boolean).forEach(function (v) {
597+
].filter(Boolean).filter(function (v, i, arr) {
598+
return arr.indexOf(v) === i;
599+
}).forEach(function (v) {
600600
var chip = document.createElement("span");
601601
chip.className = "chip";
602602
chip.textContent = v;
@@ -605,10 +605,15 @@ <h2 id="sheet-title">Choose model</h2>
605605
body.appendChild(id);
606606
body.appendChild(meta);
607607
body.appendChild(chips);
608-
if (model.reasons && model.reasons.length) {
608+
if (model.primary_reason || (model.secondary_reasons && model.secondary_reasons.length)) {
609609
var reasons = document.createElement("div");
610610
reasons.className = "model-card__note";
611-
reasons.textContent = model.reasons.slice(0, 4).join(" · ");
611+
var why = [];
612+
if (model.primary_reason) why.push("Why: " + model.primary_reason);
613+
(model.secondary_reasons || []).slice(0, 3).forEach(function (reason) {
614+
why.push(reason);
615+
});
616+
reasons.textContent = why.join(" · ");
612617
body.appendChild(reasons);
613618
}
614619
if (model.warnings && model.warnings.length) {

0 commit comments

Comments
 (0)