opencode-models 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencode_models/__init__.py +8 -0
- opencode_models/__main__.py +5 -0
- opencode_models/arena.py +108 -0
- opencode_models/auth.py +48 -0
- opencode_models/availability.py +102 -0
- opencode_models/benchmarks.py +19 -0
- opencode_models/boards.py +518 -0
- opencode_models/cli.py +1172 -0
- opencode_models/data.py +82 -0
- opencode_models/fetch.py +101 -0
- opencode_models/identity.py +100 -0
- opencode_models/labelmap.py +84 -0
- opencode_models/live.py +88 -0
- opencode_models/overrides.py +252 -0
- opencode_models/pareto.py +34 -0
- opencode_models/pricing.py +415 -0
- opencode_models/ranking.py +591 -0
- opencode_models/scoring.py +230 -0
- opencode_models/seed.py +184 -0
- opencode_models/sources/go.txt +45 -0
- opencode_models/sources/zen.txt +87 -0
- opencode_models/sources.py +55 -0
- opencode_models/tables.py +94 -0
- opencode_models-0.1.0.dist-info/METADATA +544 -0
- opencode_models-0.1.0.dist-info/RECORD +28 -0
- opencode_models-0.1.0.dist-info/WHEEL +4 -0
- opencode_models-0.1.0.dist-info/entry_points.txt +2 -0
- opencode_models-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""Rank models by score per dollar: score / input-price (or output-price),
|
|
2
|
+
descending. Free models score an infinite ratio and lead their benchmark,
|
|
3
|
+
ordered by score. Models missing the price or the benchmark's score are
|
|
4
|
+
excluded — a ratio cannot be computed for them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
|
|
11
|
+
from .data import Model
|
|
12
|
+
from .identity import canonical_id
|
|
13
|
+
from .pricing import nominal_free_rate, variant_of, weighted_cost
|
|
14
|
+
from .scoring import combined, derived_scores, scales, tracked
|
|
15
|
+
|
|
16
|
+
_PRICE_FIELDS = ("price_input", "price_output")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _price_of(m: Model, field: str) -> float | None:
|
|
20
|
+
if field not in _PRICE_FIELDS:
|
|
21
|
+
raise ValueError(f"unknown price field {field!r}")
|
|
22
|
+
return getattr(m, field)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def by_score_per_price(
|
|
26
|
+
models: list[Model], benchmark: str, price_field: str = "price_input"
|
|
27
|
+
) -> list[Model]:
|
|
28
|
+
"""Best value first. Ties on ratio break toward the higher score.
|
|
29
|
+
|
|
30
|
+
*price_field* selects the cost axis: ``price_input`` (default) or
|
|
31
|
+
``price_output``.
|
|
32
|
+
"""
|
|
33
|
+
ranked = [
|
|
34
|
+
m
|
|
35
|
+
for m in models
|
|
36
|
+
if _price_of(m, price_field) is not None and benchmark in m.scores
|
|
37
|
+
]
|
|
38
|
+
return sorted(
|
|
39
|
+
ranked,
|
|
40
|
+
key=lambda m: (
|
|
41
|
+
-float("inf")
|
|
42
|
+
if _price_of(m, price_field) == 0
|
|
43
|
+
else -m.scores[benchmark] / _price_of(m, price_field),
|
|
44
|
+
-m.scores[benchmark],
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
#: Benchmark families the console list combines, in priority order.
|
|
50
|
+
PRIMARY_FAMILY = "Terminal-Bench"
|
|
51
|
+
SECONDARY_FAMILY = "SWE-Bench"
|
|
52
|
+
#: Terminal-Bench 4.0 (Aug 2026) is a harder task set than 2.x — the best
|
|
53
|
+
#: 4.0 run scores ~58 where 2.1 leaders reach ~87 — so its numbers live on
|
|
54
|
+
#: their own scale. 2.1 still builds the skeleton (37 tier models carry it
|
|
55
|
+
#: against 12 for 4.0); 4.0 fills in like SWE-Bench until coverage flips.
|
|
56
|
+
TB4_FAMILY = "Terminal-Bench 4.0"
|
|
57
|
+
|
|
58
|
+
#: Fill-in axes, applied after the primary skeleton is built.
|
|
59
|
+
SECONDARY_AXES = (SECONDARY_FAMILY, TB4_FAMILY)
|
|
60
|
+
|
|
61
|
+
#: The ordering axes shown side by side in the console: (key, column label,
|
|
62
|
+
#: benchmark family, versioned?). ``tb2`` is versioned so only real 2.x/3.x
|
|
63
|
+
#: runs count — Terminal-Bench Hard and Science are separate benchmarks that
|
|
64
|
+
#: merely share the prefix, and 4.0 is its own scale.
|
|
65
|
+
AXES = (
|
|
66
|
+
("tb2", "TB2", PRIMARY_FAMILY, True),
|
|
67
|
+
("tb4", "TB4", TB4_FAMILY, False),
|
|
68
|
+
("swe", "SWE", SECONDARY_FAMILY, False),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
#: Share of the list culled into each side table.
|
|
72
|
+
CULL_FRACTION = 0.3
|
|
73
|
+
|
|
74
|
+
#: Rank grades for the console's marker column, read off the 0-100 score
|
|
75
|
+
#: itself rather than off position in the list: 90+ is `++`, 75+ is `+`,
|
|
76
|
+
#: 60+ is `o`, 50+ is `-`, below that `--`. Absolute bands mean a strong
|
|
77
|
+
#: field can share the top and a weak one is never flattered by it.
|
|
78
|
+
GRADE_PLUSPLUS = 90.0
|
|
79
|
+
GRADE_PLUS = 75.0
|
|
80
|
+
GRADE_O = 60.0
|
|
81
|
+
GRADE_MINUS = 50.0
|
|
82
|
+
|
|
83
|
+
BAND_FREE = "free"
|
|
84
|
+
BAND_INCLUDED = "included"
|
|
85
|
+
BAND_PAYG = "payg"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class Row:
|
|
90
|
+
"""One printable line of the console list."""
|
|
91
|
+
|
|
92
|
+
model: Model
|
|
93
|
+
name: str # display label: variant and [free] applied
|
|
94
|
+
price: float # representative cost, $/1M (nominal if free)
|
|
95
|
+
free: bool
|
|
96
|
+
band: str
|
|
97
|
+
score: float | None # primary family score, else secondary
|
|
98
|
+
benchmark: str | None # the key `score` came from
|
|
99
|
+
grade: str = "o" # rank marker: ++ / + / o / -- / ·
|
|
100
|
+
provider: str = "-" # zen / go, "-" when in neither tier
|
|
101
|
+
entry: object | None = None # pricing.PriceEntry when doc-priced
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _best(
|
|
105
|
+
scores: dict[str, float],
|
|
106
|
+
family: str,
|
|
107
|
+
skip: str | None = None,
|
|
108
|
+
versioned: bool = False,
|
|
109
|
+
) -> tuple[str, float] | None:
|
|
110
|
+
"""Highest score under *family*, as ``(key, value)``.
|
|
111
|
+
|
|
112
|
+
Families are matched with spaces folded to dashes so ``Terminal Bench
|
|
113
|
+
2.1`` and ``Terminal-Bench 2.1`` count as the same benchmark.
|
|
114
|
+
|
|
115
|
+
*skip* excludes keys on a harder scale of the same family — a 4.0 score
|
|
116
|
+
must not win a slot that is compared against 2.x numbers.
|
|
117
|
+
|
|
118
|
+
*versioned* restricts the family to actual version runs: after the
|
|
119
|
+
family prefix a version number must follow. Terminal-Bench Hard and
|
|
120
|
+
Terminal-Bench-Science are separate benchmarks that happen to share the
|
|
121
|
+
prefix — a 20.5 on TB Hard is not a "Terminal-Bench 2.x" run and must
|
|
122
|
+
never fill a slot compared against 2.1 board numbers.
|
|
123
|
+
"""
|
|
124
|
+
best = None
|
|
125
|
+
folded_skip = skip.lower().replace(" ", "-") if skip else None
|
|
126
|
+
folded_family = family.lower().replace(" ", "-")
|
|
127
|
+
for key, value in scores.items():
|
|
128
|
+
folded = key.lower().replace(" ", "-")
|
|
129
|
+
if not folded.startswith(folded_family):
|
|
130
|
+
continue
|
|
131
|
+
if versioned and not _version_run(folded[len(folded_family) :]):
|
|
132
|
+
continue
|
|
133
|
+
if folded_skip and folded.startswith(folded_skip):
|
|
134
|
+
continue
|
|
135
|
+
if best is None or value > best[1]:
|
|
136
|
+
best = (key, value)
|
|
137
|
+
return best
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _version_run(after_family: str) -> bool:
|
|
141
|
+
"""True when what follows the family prefix is a version, not a word.
|
|
142
|
+
|
|
143
|
+
``-2.1`` and ``-3.0`` are version runs; ``-Hard`` and ``-Science-0.1``
|
|
144
|
+
are different benchmarks sharing the prefix.
|
|
145
|
+
"""
|
|
146
|
+
return after_family.lstrip("-_ ")[:1].isdigit()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def ratio(score: float, price: float) -> float:
|
|
150
|
+
"""Score per dollar; a free line beats everything rather than going inf..."""
|
|
151
|
+
return float("inf") if price == 0 else score / price
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_rows(
|
|
155
|
+
models: list[Model],
|
|
156
|
+
entries=(),
|
|
157
|
+
free_ids=frozenset(),
|
|
158
|
+
axis: str = "weighted",
|
|
159
|
+
provider: str = "-",
|
|
160
|
+
table: dict | None = None,
|
|
161
|
+
free_rates: dict[str, float] | None = None,
|
|
162
|
+
) -> list[Row]:
|
|
163
|
+
"""Price every line of the console list.
|
|
164
|
+
|
|
165
|
+
A documented line replaces the catalog price (the catalog has no notion
|
|
166
|
+
of tier: it carries the Zen rate for a model Go sells cheaper), and one
|
|
167
|
+
model can produce several rows — one per priced variant. Free rows carry
|
|
168
|
+
their model's nominal free-tier rate (pricing.FREE_TIER_NOMINAL_RATES):
|
|
169
|
+
free models are not equally expensive to serve — Step-5-Free burns ~70x
|
|
170
|
+
what Muse-Spark-Contributor-Free does per request — so sharing one price
|
|
171
|
+
would rank them as equals. Every nominal sits below the cheapest paid
|
|
172
|
+
rate, so free still beats paid on value, just not all alike.
|
|
173
|
+
|
|
174
|
+
*axis* picks what a line costs: ``weighted`` (20/60/20 in/cached/out),
|
|
175
|
+
or a single ``input`` / ``output`` rate.
|
|
176
|
+
"""
|
|
177
|
+
by_model: dict[str, list] = {}
|
|
178
|
+
for entry in entries:
|
|
179
|
+
if entry.model_id:
|
|
180
|
+
by_model.setdefault(entry.model_id, []).append(entry)
|
|
181
|
+
|
|
182
|
+
def cost_of(model: Model, entry) -> float | None:
|
|
183
|
+
if axis == "input":
|
|
184
|
+
return entry.input if entry else model.price_input
|
|
185
|
+
if axis == "output":
|
|
186
|
+
return entry.output if entry else model.price_output
|
|
187
|
+
if entry:
|
|
188
|
+
return weighted_cost(entry.input, entry.output, entry.cached_read)
|
|
189
|
+
return weighted_cost(
|
|
190
|
+
model.price_input, model.price_output, model.price_cached_input
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
staged = [] # (model, entry, display name, cost, free, monthly limit)
|
|
194
|
+
for m in models:
|
|
195
|
+
slug = m.model_id.split("/")[-1]
|
|
196
|
+
documented = by_model.get(slug, []) + by_model.get(f"{slug}-free", [])
|
|
197
|
+
cost = cost_of(m, None)
|
|
198
|
+
if not documented:
|
|
199
|
+
if cost is None:
|
|
200
|
+
continue # no price at all — nothing to rank it on
|
|
201
|
+
staged.append(
|
|
202
|
+
(m, None, m.name, cost, m.model_id in free_ids or cost == 0, None)
|
|
203
|
+
)
|
|
204
|
+
continue
|
|
205
|
+
variants = set()
|
|
206
|
+
for entry in documented: # first tier in *entries* owns a variant
|
|
207
|
+
suffix = variant_of(entry.name)
|
|
208
|
+
if suffix in variants:
|
|
209
|
+
continue
|
|
210
|
+
variants.add(suffix)
|
|
211
|
+
label = f"{m.name} {suffix}" if suffix else m.name
|
|
212
|
+
staged.append(
|
|
213
|
+
(
|
|
214
|
+
m,
|
|
215
|
+
entry,
|
|
216
|
+
label,
|
|
217
|
+
cost_of(m, entry),
|
|
218
|
+
entry.free or m.model_id in free_ids,
|
|
219
|
+
entry.limit,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
rows = []
|
|
224
|
+
for model, entry, name, cost, free, limit in staged:
|
|
225
|
+
if free:
|
|
226
|
+
price, band = nominal_free_rate(model.model_id, free_rates), BAND_FREE
|
|
227
|
+
if "free" not in name.lower():
|
|
228
|
+
name = f"{name} [free]"
|
|
229
|
+
else:
|
|
230
|
+
price = cost
|
|
231
|
+
band = BAND_INCLUDED if limit is not None else BAND_PAYG
|
|
232
|
+
# the row's score is the model's own: every benchmark it carries on
|
|
233
|
+
# that benchmark's 0-100 scale, median across them. *table* is the
|
|
234
|
+
# scale of the whole catalog, passed in so every view grades against
|
|
235
|
+
# the same yardstick.
|
|
236
|
+
derived = derived_scores(model, table or scales(models))
|
|
237
|
+
score = combined(derived)
|
|
238
|
+
strongest = max(derived, key=lambda k: derived[k]) if derived else None
|
|
239
|
+
rows.append(
|
|
240
|
+
Row(
|
|
241
|
+
model=model,
|
|
242
|
+
name=name,
|
|
243
|
+
price=price,
|
|
244
|
+
free=free,
|
|
245
|
+
band=band,
|
|
246
|
+
score=score,
|
|
247
|
+
benchmark=strongest,
|
|
248
|
+
provider=provider,
|
|
249
|
+
entry=entry,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return rows
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def merge_rows(*groups: list[Row]) -> list[Row]:
|
|
256
|
+
"""Concatenate provider groups onto one free-price scale.
|
|
257
|
+
|
|
258
|
+
Free rows already carry their model's nominal rate from build_rows,
|
|
259
|
+
which is per-model rather than per-group, so there is nothing to
|
|
260
|
+
re-price: the whole view agrees by construction. Provider tags and order
|
|
261
|
+
are kept.
|
|
262
|
+
"""
|
|
263
|
+
return [row for group in groups for row in group]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
#: How generous a band is, for picking the best deal out of a merged line.
|
|
267
|
+
_BAND_RANK = {BAND_FREE: 0, BAND_INCLUDED: 1, BAND_PAYG: 2}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def dedupe_providers(rows: list[Row]) -> list[Row]:
|
|
271
|
+
"""One line per (model, variant, score, cost), providers enumerated.
|
|
272
|
+
|
|
273
|
+
zen and go sell many models at the same rate for the same score, so the
|
|
274
|
+
pair is one offer rather than two: the providers are listed together
|
|
275
|
+
(``go,zen``). When only some of the providers fund the line from a
|
|
276
|
+
monthly allowance, the more generous band wins — the line is available
|
|
277
|
+
that way from at least one of the providers named on it.
|
|
278
|
+
"""
|
|
279
|
+
merged: dict[tuple, Row] = {}
|
|
280
|
+
keys: list[tuple] = []
|
|
281
|
+
for row in rows:
|
|
282
|
+
key = (row.model.model_id, row.name, row.score, round(row.price, 9))
|
|
283
|
+
if key not in merged:
|
|
284
|
+
merged[key] = row
|
|
285
|
+
keys.append(key)
|
|
286
|
+
continue
|
|
287
|
+
current = merged[key]
|
|
288
|
+
keep = current if _BAND_RANK[current.band] <= _BAND_RANK[row.band] else row
|
|
289
|
+
providers = sorted(
|
|
290
|
+
{p for side in (current.provider, row.provider) for p in side.split(",")}
|
|
291
|
+
)
|
|
292
|
+
merged[key] = replace(keep, provider=",".join(providers))
|
|
293
|
+
return [merged[key] for key in keys]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def order_rows(rows: list[Row]) -> list[Row]:
|
|
297
|
+
"""Best score per dollar first; the rest cannot be valued.
|
|
298
|
+
|
|
299
|
+
The score is already comparable across axes — each benchmark reads on
|
|
300
|
+
its own 0-100 scale and the median combines them — so there is no
|
|
301
|
+
skeleton to build and no percentile to insert at. Value order is the
|
|
302
|
+
combined score divided by the price, ties toward the higher score, and
|
|
303
|
+
a free scored line leads everything paid (infinite value sorts first,
|
|
304
|
+
not last). Rows no benchmark reaches cannot be valued at all: free ones
|
|
305
|
+
close the listing (they cost nothing whatever — maximising free usage
|
|
306
|
+
means seeing them), paid ones follow behind, and rows nothing reaches
|
|
307
|
+
and nobody sells free drop out.
|
|
308
|
+
"""
|
|
309
|
+
scored = sorted(
|
|
310
|
+
(r for r in rows if r.score is not None),
|
|
311
|
+
key=lambda r: (-ratio(r.score, r.price), -r.score),
|
|
312
|
+
)
|
|
313
|
+
listed = {id(r) for r in scored}
|
|
314
|
+
tail = [
|
|
315
|
+
r
|
|
316
|
+
for r in rows
|
|
317
|
+
if r.score is None and id(r) not in listed and (r.free or _measured(r))
|
|
318
|
+
]
|
|
319
|
+
free = [r for r in tail if r.free]
|
|
320
|
+
paid = [r for r in tail if not r.free]
|
|
321
|
+
return scored + free + paid
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _measured(row: Row) -> bool:
|
|
325
|
+
"""Whether any scored benchmark reaches this row's model."""
|
|
326
|
+
return any(tracked(name) for name in row.model.scores)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def axis_scores(model: Model) -> dict[str, float | None]:
|
|
330
|
+
"""Each axis's raw score for *model*, None where it carries none.
|
|
331
|
+
|
|
332
|
+
The console prints these side by side so the viewer sees the base
|
|
333
|
+
scores the ordering is built from instead of one merged number.
|
|
334
|
+
"""
|
|
335
|
+
out: dict[str, float | None] = {}
|
|
336
|
+
for key, _label, family, versioned in AXES:
|
|
337
|
+
best = _best(
|
|
338
|
+
model.scores,
|
|
339
|
+
family,
|
|
340
|
+
skip=TB4_FAMILY if key == "tb2" else None,
|
|
341
|
+
versioned=versioned,
|
|
342
|
+
)
|
|
343
|
+
out[key] = best[1] if best else None
|
|
344
|
+
return out
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def grade_for(score: float | None) -> str:
|
|
348
|
+
"""One score's marker: the band it clears, or ``·`` with no score."""
|
|
349
|
+
if score is None:
|
|
350
|
+
return "·"
|
|
351
|
+
if score >= GRADE_PLUSPLUS:
|
|
352
|
+
return "++"
|
|
353
|
+
if score >= GRADE_PLUS:
|
|
354
|
+
return "+"
|
|
355
|
+
if score >= GRADE_O:
|
|
356
|
+
return "o"
|
|
357
|
+
if score >= GRADE_MINUS:
|
|
358
|
+
return "-"
|
|
359
|
+
return "--"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def grade_rows(rows: list[Row]) -> list[Row]:
|
|
363
|
+
"""Tag every row with a marker for how strong it is, keeping the order.
|
|
364
|
+
|
|
365
|
+
The bands come from the *score*, not from position in the table. Price
|
|
366
|
+
sorts the table — that is the Pareto view — but a model that scores well
|
|
367
|
+
should not be graded "middle" for costing money, and a cheap model
|
|
368
|
+
should not be graded "top" for being cheap.
|
|
369
|
+
|
|
370
|
+
Rows with no score are unranked rather than last: they show ``·``, so a
|
|
371
|
+
free model nobody benchmarks is not mistaken for the bottom.
|
|
372
|
+
"""
|
|
373
|
+
return [replace(r, grade=grade_for(r.score)) for r in rows]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
#: How much weaker than the strongest model known a pick may be, as a
|
|
377
|
+
#: fraction of its score.
|
|
378
|
+
#:
|
|
379
|
+
#: Eligibility is deliberately *strength*, not the rank marker. The marker
|
|
380
|
+
#: is a position in an ordering that divides by price, so it measures value:
|
|
381
|
+
#: in the live ordering MiMo-V2.6-Pro scores 89.9 — the best model in the
|
|
382
|
+
#: catalog — and is graded ``o``, while a free model scoring 24.58 is graded
|
|
383
|
+
#: ``++``. Gating on it recommends the weak model and misses the strong one.
|
|
384
|
+
RECOMMEND_FLOOR = 0.75
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def next_up(
|
|
388
|
+
rows: list[Row],
|
|
389
|
+
count: int = 4,
|
|
390
|
+
grade: str | None = None,
|
|
391
|
+
floor: float = RECOMMEND_FLOOR,
|
|
392
|
+
) -> list[Row]:
|
|
393
|
+
"""The next *count* models to use, in order.
|
|
394
|
+
|
|
395
|
+
Candidates alternate between the free and paid bands slot by slot —
|
|
396
|
+
even slots take the best remaining free pick, odd slots the best
|
|
397
|
+
remaining paid one — so a capped free tier still leaves half the
|
|
398
|
+
sequence usable, and a fresh one still leads with what costs nothing.
|
|
399
|
+
Free picks order by score, since they cost nothing and only the score
|
|
400
|
+
is left to maximise; paid picks order by score per dollar, the measure
|
|
401
|
+
the list view already ranks on.
|
|
402
|
+
|
|
403
|
+
That order is then alternated between Zen and Go wherever both plans
|
|
404
|
+
can supply a pick, so following the sequence spreads across the two
|
|
405
|
+
instead of draining one allowance before touching the other. Plan
|
|
406
|
+
alternation is a preference, not a rule: it gives way when the wanted
|
|
407
|
+
band has nothing on the other plan, and band alternation gives way
|
|
408
|
+
when a band runs dry — the other band fills the slot instead.
|
|
409
|
+
|
|
410
|
+
Eligibility is strength, not the rank marker: a pick must be within
|
|
411
|
+
*floor* of the strongest score in *rows*, and may be narrowed further by
|
|
412
|
+
a *grade*. A short list is a short list, not an error.
|
|
413
|
+
"""
|
|
414
|
+
scored = [r for r in rows if r.score is not None]
|
|
415
|
+
if not scored:
|
|
416
|
+
return []
|
|
417
|
+
# the reference is the strongest model in the whole catalog, so the bar
|
|
418
|
+
# cannot be dragged down by a weak set of top rows
|
|
419
|
+
best = max(r.score for r in scored)
|
|
420
|
+
eligible = [r for r in scored if r.score >= best * floor]
|
|
421
|
+
if grade:
|
|
422
|
+
eligible = [r for r in eligible if r.grade == grade]
|
|
423
|
+
free = sorted((r for r in eligible if r.free), key=lambda r: (-r.score, r.name))
|
|
424
|
+
paid = sorted(
|
|
425
|
+
(r for r in eligible if not r.free),
|
|
426
|
+
key=lambda r: (-ratio(r.score, r.price), -r.score, r.name),
|
|
427
|
+
)
|
|
428
|
+
# Band alternation is the slot order; the plan alternation inside each
|
|
429
|
+
# slot prefers a plan the previous pick did not touch.
|
|
430
|
+
return _take_banded(free, paid, count)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def plans_of(row: Row) -> set[str]:
|
|
434
|
+
"""The plans a row is sold on; a shared offer counts for each of them.
|
|
435
|
+
|
|
436
|
+
``dedupe_providers`` labels an offer both tiers sell at the same rate as
|
|
437
|
+
``"go,zen"``. Treating that as a plan of its own would leave it out of
|
|
438
|
+
the alternation entirely, so it is read as membership of both.
|
|
439
|
+
"""
|
|
440
|
+
return set(row.provider.split(","))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _take_banded(free: list[Row], paid: list[Row], want: int) -> list[Row]:
|
|
444
|
+
"""Up to *want* rows, alternating free and paid slots.
|
|
445
|
+
|
|
446
|
+
One model is recommended once, however many lines it appears on: free on
|
|
447
|
+
Zen and paid on Go are two lines for one model, and spending two slots
|
|
448
|
+
on it wastes a recommendation. Even slots draw from the free pool, odd
|
|
449
|
+
slots from the paid pool; a dry band yields its slot to the other pool
|
|
450
|
+
rather than ending the sequence short.
|
|
451
|
+
|
|
452
|
+
Within a slot the plan alternates too: the first unseen row on a plan
|
|
453
|
+
the previous pick did not touch wins, falling back to the strongest
|
|
454
|
+
unseen row in the pool when every plan left touches it. The leading pick
|
|
455
|
+
is the strongest free row available, whichever plan it sits on. A plan
|
|
456
|
+
with nothing left is simply passed over — there is nothing to
|
|
457
|
+
alternate with.
|
|
458
|
+
"""
|
|
459
|
+
pools = [list(free), list(paid)]
|
|
460
|
+
seen: set[str] = set()
|
|
461
|
+
taken: list[Row] = []
|
|
462
|
+
previous: set[str] = set()
|
|
463
|
+
for slot in range(want):
|
|
464
|
+
first, second = pools if slot % 2 == 0 else pools[::-1]
|
|
465
|
+
row = (
|
|
466
|
+
_off_plan(first, seen, previous)
|
|
467
|
+
or _first_unseen(first, seen)
|
|
468
|
+
or _off_plan(second, seen, previous)
|
|
469
|
+
or _first_unseen(second, seen)
|
|
470
|
+
)
|
|
471
|
+
if row is None:
|
|
472
|
+
break
|
|
473
|
+
taken.append(row)
|
|
474
|
+
seen.add(canonical_id(row.model.model_id))
|
|
475
|
+
previous = plans_of(row)
|
|
476
|
+
return taken
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _first_unseen(pool: list[Row], seen: set[str]) -> Row | None:
|
|
480
|
+
"""The strongest row in *pool* for a model not yet recommended."""
|
|
481
|
+
for row in pool:
|
|
482
|
+
if canonical_id(row.model.model_id) not in seen:
|
|
483
|
+
return row
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _off_plan(pool: list[Row], seen: set[str], previous: set[str]) -> Row | None:
|
|
488
|
+
"""The strongest unseen row in *pool* on a fresh plan.
|
|
489
|
+
|
|
490
|
+
Nothing is fresh before the first pick, so the lead always falls
|
|
491
|
+
through to the strongest row outright.
|
|
492
|
+
"""
|
|
493
|
+
for row in pool:
|
|
494
|
+
if (
|
|
495
|
+
canonical_id(row.model.model_id) not in seen
|
|
496
|
+
and not plans_of(row) & previous
|
|
497
|
+
):
|
|
498
|
+
return row
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _take(ordered: list[Row], want: int) -> list[Row]:
|
|
503
|
+
"""Up to *want* rows from *ordered*, alternating between plans.
|
|
504
|
+
|
|
505
|
+
One model is recommended once, however many lines it appears on: free on
|
|
506
|
+
Zen and paid on Go are two lines for one model, and spending two slots
|
|
507
|
+
on it wastes a recommendation. Free-first ordering means the cheaper
|
|
508
|
+
line is the one already in *ordered*, so it is the one that survives.
|
|
509
|
+
|
|
510
|
+
Plans are visited strongest-first, so the leading pick is the best row
|
|
511
|
+
available whichever plan it sits on. A plan with nothing left is simply
|
|
512
|
+
passed over — there is nothing to alternate with.
|
|
513
|
+
"""
|
|
514
|
+
remaining = list(ordered)
|
|
515
|
+
seen: set[str] = set()
|
|
516
|
+
unique: list[Row] = []
|
|
517
|
+
for row in remaining:
|
|
518
|
+
if canonical_id(row.model.model_id) in seen:
|
|
519
|
+
continue
|
|
520
|
+
seen.add(canonical_id(row.model.model_id))
|
|
521
|
+
unique.append(row)
|
|
522
|
+
remaining = unique
|
|
523
|
+
plans = sorted(
|
|
524
|
+
{p for row in remaining for p in plans_of(row)},
|
|
525
|
+
key=lambda p: max((r.score for r in remaining if p in plans_of(r)), default=0),
|
|
526
|
+
reverse=True,
|
|
527
|
+
)
|
|
528
|
+
taken: list[Row] = []
|
|
529
|
+
while len(taken) < want and remaining:
|
|
530
|
+
progressed = False
|
|
531
|
+
for plan in plans:
|
|
532
|
+
if len(taken) >= want:
|
|
533
|
+
break
|
|
534
|
+
for i, row in enumerate(remaining):
|
|
535
|
+
if plan in plans_of(row):
|
|
536
|
+
taken.append(row)
|
|
537
|
+
remaining.pop(i)
|
|
538
|
+
progressed = True
|
|
539
|
+
break
|
|
540
|
+
if not progressed:
|
|
541
|
+
break
|
|
542
|
+
return taken
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def partition_rows(
|
|
546
|
+
rows: list[Row], fraction: float = CULL_FRACTION
|
|
547
|
+
) -> tuple[list[Row], list[Row], list[Row]]:
|
|
548
|
+
"""Split into (best value, worst score, highest cost).
|
|
549
|
+
|
|
550
|
+
The split always happens: the worst-scoring and priciest models are
|
|
551
|
+
taken out of best value whether or not a caller goes on to print them.
|
|
552
|
+
A view that shows one table is still a view that hid those rows, which is
|
|
553
|
+
the point of the cull.
|
|
554
|
+
|
|
555
|
+
Each cull takes the nearest whole row to *fraction* of the list (one row
|
|
556
|
+
minimum once there is more than one, never on a single row). The groups
|
|
557
|
+
are disjoint and the cost group has first claim: a model that is both
|
|
558
|
+
priciest and lowest-scoring is reported once, under cost.
|
|
559
|
+
"""
|
|
560
|
+
count = len(rows)
|
|
561
|
+
take = 0 if count <= 1 else max(1, round(fraction * count))
|
|
562
|
+
|
|
563
|
+
expensive = sorted(
|
|
564
|
+
range(count),
|
|
565
|
+
key=lambda i: (
|
|
566
|
+
-rows[i].price,
|
|
567
|
+
rows[i].score if rows[i].score is not None else float("inf"),
|
|
568
|
+
),
|
|
569
|
+
)[:take]
|
|
570
|
+
claimed = set(expensive)
|
|
571
|
+
|
|
572
|
+
rest = [i for i in range(count) if i not in claimed]
|
|
573
|
+
weakest = sorted(
|
|
574
|
+
rest,
|
|
575
|
+
key=lambda i: (
|
|
576
|
+
rows[i].score if rows[i].score is not None else float("inf"),
|
|
577
|
+
-rows[i].price,
|
|
578
|
+
),
|
|
579
|
+
)[:take]
|
|
580
|
+
weakest_set = set(weakest)
|
|
581
|
+
|
|
582
|
+
main = [rows[i] for i in rest if i not in weakest_set]
|
|
583
|
+
# both cull tables read worst-scoring first; the expensive group still
|
|
584
|
+
# *selects* on price, it just no longer sorts by it
|
|
585
|
+
by_score = lambda i: ( # noqa: E731
|
|
586
|
+
rows[i].score if rows[i].score is not None else float("inf"),
|
|
587
|
+
-rows[i].price,
|
|
588
|
+
)
|
|
589
|
+
worst = [rows[i] for i in sorted(weakest, key=by_score)]
|
|
590
|
+
pricey = [rows[i] for i in sorted(expensive, key=by_score)]
|
|
591
|
+
return main, worst, pricey
|