Skip to content

Commit 30f45b9

Browse files
committed
feat(models): improve catalog browsing and pricing
1 parent 1bdb584 commit 30f45b9

22 files changed

Lines changed: 392 additions & 58 deletions

File tree

src/app/[locale]/models/[slug]/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ export async function generateMetadata({
4444
description: model.description || '',
4545
vendor: model.vendor,
4646
size: model.size ?? undefined,
47+
activeParameters: model.activeParameters ?? undefined,
4748
contextWindow: model.contextWindow,
4849
maxOutput: model.maxOutput ?? undefined,
4950
lifecycle: model.lifecycle,
5051
tokenPricing: model.tokenPricing,
52+
referenceTokenPricing: model.referenceTokenPricing,
5153
},
5254
translationNamespace: 'pages.modelDetail',
5355
})
@@ -84,7 +86,15 @@ export default async function ModelPage({
8486

8587
// Build additional info for ProductHero
8688
const additionalInfo = [
87-
model.size && { label: tShared('terms.modelSize'), value: model.size },
89+
model.size && {
90+
label: tShared('terms.modelSize'),
91+
value: model.activeParameters
92+
? tShared('modelParameters.totalAndActive', {
93+
total: model.size,
94+
active: model.activeParameters,
95+
})
96+
: model.size,
97+
},
8898
{
8999
label: tShared('terms.contextWindow'),
90100
value: `${model.contextWindow.toLocaleString()} tokens`,

src/app/[locale]/models/compare/[models]/page.client.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Locale } from '@/i18n/config'
99
import { Link, useRouter } from '@/i18n/navigation'
1010
import { providersData } from '@/lib/generated'
1111
import { buildModelComparisonPath } from '@/lib/model-comparison'
12-
import { type CurrencyConversion, formatPrimaryTokenRate } from '@/lib/model-pricing'
12+
import { type CurrencyConversion, formatModelTokenRate } from '@/lib/model-pricing'
1313
import type {
1414
ManifestBenchmarks,
1515
ManifestModel,
@@ -55,6 +55,7 @@ const FALLBACK_MODEL: ManifestModel = {
5555
websiteUrl: '',
5656
docsUrl: null,
5757
size: '',
58+
activeParameters: null,
5859
contextWindow: 0,
5960
maxOutput: 0,
6061
tokenPricing: EMPTY_TOKEN_PRICING,
@@ -106,7 +107,7 @@ const createPriceRenderer = (
106107
formatPerMillion: (price: string) => string
107108
) => {
108109
return (model: ManifestModel) => {
109-
const price = formatPrimaryTokenRate(model.tokenPricing, rate, locale, conversion)
110+
const price = formatModelTokenRate(model, rate, locale, conversion)
110111
return price ? formatPerMillion(price) : '-'
111112
}
112113
}
@@ -277,7 +278,14 @@ export default function ComparePageClient({
277278
groupLabel: tShared('capabilities.capabilities'),
278279
key: 'size',
279280
label: tShared('terms.modelSize'),
280-
render: createSimpleRenderer(m => m.size),
281+
render: createSimpleRenderer(m =>
282+
m.size && m.activeParameters
283+
? tShared('modelParameters.totalAndActive', {
284+
total: m.size,
285+
active: m.activeParameters,
286+
})
287+
: m.size
288+
),
281289
},
282290
{
283291
group: 'capabilities',

src/app/[locale]/models/page.client.tsx

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { formatTokenCount } from '@/lib/format'
1616
import { modelsData } from '@/lib/generated'
1717
import { localizeManifestItems } from '@/lib/manifest-i18n'
1818
import { buildModelComparisonPath } from '@/lib/model-comparison'
19-
import { formatPrimaryTokenRate } from '@/lib/model-pricing'
19+
import { groupModelsByLifecycle } from '@/lib/model-list'
20+
import { formatModelTokenRate } from '@/lib/model-pricing'
2021
import type { ManifestModel } from '@/types/manifests'
2122

2223
type Props = {
@@ -67,21 +68,10 @@ export default function ModelsPageClient({ locale }: Props) {
6768
}, [localizedModels, searchQuery])
6869

6970
// Group filtered models by lifecycle
70-
const modelsByLifecycle = useMemo(() => {
71-
const groups = {
72-
latest: [] as ManifestModel[],
73-
maintained: [] as ManifestModel[],
74-
deprecated: [] as ManifestModel[],
75-
}
76-
filteredModels.forEach(model => {
77-
const lifecycle = model.lifecycle || 'maintained'
78-
groups[lifecycle].push(model)
79-
})
80-
return groups
81-
}, [filteredModels])
71+
const modelsByLifecycle = useMemo(() => groupModelsByLifecycle(filteredModels), [filteredModels])
8272

8373
const formatListPrice = (model: ManifestModel): string => {
84-
const price = formatPrimaryTokenRate(model.tokenPricing, 'input', locale, conversion)
74+
const price = formatModelTokenRate(model, 'input', locale, conversion)
8575
return price
8676
? tShared('modelPricing.perMillionTokens', { price })
8777
: tShared('modelPricing.notAvailable')
@@ -109,11 +99,11 @@ export default function ModelsPageClient({ locale }: Props) {
10999

110100
<StackTabs activeStack="models" locale={locale} />
111101

112-
<section className="mb-[var(--spacing-lg)] max-w-6xl border-y border-[var(--color-border)] py-[var(--spacing-md)]">
102+
<section className="mb-[var(--spacing-lg)] border-y border-[var(--color-border)] py-[var(--spacing-md)]">
113103
<h2 className="mb-[var(--spacing-xs)] text-lg font-semibold tracking-tight">
114104
{tPage('guide.title')}
115105
</h2>
116-
<p className="max-w-4xl text-sm leading-relaxed text-[var(--color-text-secondary)]">
106+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
117107
{tPage('guide.description')}
118108
</p>
119109
<div className="mt-[var(--spacing-sm)] flex flex-wrap gap-[var(--spacing-md)] text-sm">
@@ -150,28 +140,35 @@ export default function ModelsPageClient({ locale }: Props) {
150140
{tShared(`lifecycle.${lifecycle}`)}
151141
</h2>
152142
{modelsByLifecycle[lifecycle].length > 0 ? (
153-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-[var(--spacing-md)]">
143+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-[var(--spacing-md)]">
154144
{modelsByLifecycle[lifecycle].map(model => (
155145
<article
156146
key={model.name}
157-
className={`border p-[var(--spacing-md)] transition-all hover:-translate-y-0.5 group ${
147+
className={`group flex flex-col border p-[var(--spacing-md)] transition-all hover:-translate-y-0.5 ${
158148
selectedIds.includes(model.id)
159149
? 'border-[var(--color-border-strong)] bg-[var(--color-hover)]'
160150
: 'border-[var(--color-border)] hover:border-[var(--color-border-strong)]'
161151
}`}
162152
>
163-
<Link href={`/models/${model.id}`} className="block">
153+
<Link href={`/models/${model.id}`} className="block flex-1">
164154
<div className="flex items-start mb-[var(--spacing-sm)]">
165-
<div className="flex items-center gap-[var(--spacing-xs)]">
166-
<h3 className="text-lg font-semibold tracking-tight">{model.name}</h3>
155+
<div className="inline-flex items-center gap-[var(--spacing-xs)] whitespace-nowrap">
156+
<h3 className="text-lg font-semibold tracking-tight whitespace-nowrap">
157+
{model.name}
158+
</h3>
167159
{model.verified && <VerifiedBadge size="sm" />}
168160
</div>
169161
</div>
170-
<div className="space-y-[var(--spacing-xs)] mb-[var(--spacing-md)]">
162+
<div className="mb-[var(--spacing-sm)] space-y-[var(--spacing-xs)]">
171163
<div className="flex items-center gap-[var(--spacing-sm)] text-xs">
172164
<span className="text-[var(--color-text-muted)]">{tPage('size')}</span>
173165
<span className="text-[var(--color-text-secondary)]">
174-
{model.size ?? '—'}
166+
{model.size && model.activeParameters
167+
? tShared('modelParameters.totalAndActive', {
168+
total: model.size,
169+
active: model.activeParameters,
170+
})
171+
: (model.size ?? '—')}
175172
</span>
176173
</div>
177174
<div className="flex items-center gap-[var(--spacing-sm)] text-xs">
@@ -191,24 +188,26 @@ export default function ModelsPageClient({ locale }: Props) {
191188
</span>
192189
</div>
193190
</div>
194-
<div className="flex items-center gap-[var(--spacing-xs)] text-xs text-[var(--color-text-muted)]">
195-
<span>{model.vendor}</span>
196-
</div>
197191
</Link>
198-
<button
199-
type="button"
200-
aria-pressed={selectedIds.includes(model.id)}
201-
title={`${tShared('actions.compare')}: ${model.name}`}
202-
onClick={() => toggleModel(model.id)}
203-
className="mt-[var(--spacing-sm)] inline-flex w-full items-center justify-center gap-[var(--spacing-xs)] border-t border-[var(--color-border)] pt-[var(--spacing-sm)] text-xs font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text)]"
204-
>
205-
{selectedIds.includes(model.id) ? (
206-
<Check className="h-3.5 w-3.5" aria-hidden="true" />
207-
) : (
208-
<Scale className="h-3.5 w-3.5" aria-hidden="true" />
209-
)}
210-
{tShared('actions.compare')}
211-
</button>
192+
<div className="mt-[var(--spacing-xs)] flex items-center justify-between gap-[var(--spacing-sm)] border-t border-[var(--color-border)] pt-[var(--spacing-sm)]">
193+
<span className="text-xs text-[var(--color-text-muted)]">
194+
{model.vendor}
195+
</span>
196+
<button
197+
type="button"
198+
aria-pressed={selectedIds.includes(model.id)}
199+
title={`${tShared('actions.compare')}: ${model.name}`}
200+
onClick={() => toggleModel(model.id)}
201+
className="inline-flex items-center gap-[var(--spacing-xs)] text-xs font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text)]"
202+
>
203+
{selectedIds.includes(model.id) ? (
204+
<Check className="h-3.5 w-3.5" aria-hidden="true" />
205+
) : (
206+
<Scale className="h-3.5 w-3.5" aria-hidden="true" />
207+
)}
208+
{tShared('actions.compare')}
209+
</button>
210+
</div>
212211
</article>
213212
))}
214213
</div>

src/components/product/ModelSpecifications.tsx

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@
33
import { useLocale, useTranslations } from 'next-intl'
44
import { useCurrency } from '@/components/CurrencyProvider'
55
import { formatTokenCount } from '@/lib/format'
6-
import { convertTokenRate, formatTokenRate, isTokenPricingAvailable } from '@/lib/model-pricing'
6+
import {
7+
convertTokenRate,
8+
formatReferenceTokenRate,
9+
formatTokenRate,
10+
isTokenPricingAvailable,
11+
} from '@/lib/model-pricing'
712
import type {
813
ManifestModel,
914
ManifestTokenPricingCondition,
1015
ManifestTokenPricingReason,
1116
} from '@/types/manifests'
1217

1318
export interface ModelSpecificationsProps {
14-
model: Pick<ManifestModel, 'size' | 'contextWindow' | 'maxOutput' | 'tokenPricing' | 'lifecycle'>
19+
model: Pick<
20+
ManifestModel,
21+
| 'size'
22+
| 'activeParameters'
23+
| 'contextWindow'
24+
| 'maxOutput'
25+
| 'tokenPricing'
26+
| 'referenceTokenPricing'
27+
| 'lifecycle'
28+
>
1529
}
1630

1731
const reasonKeys: Record<ManifestTokenPricingReason, string> = {
@@ -32,7 +46,14 @@ export function ModelSpecifications({ model }: ModelSpecificationsProps) {
3246
const locale = useLocale()
3347
const tShared = useTranslations('shared')
3448
const { conversion } = useCurrency()
35-
const hasContent = model.size || model.contextWindow || model.maxOutput || model.tokenPricing
49+
const parameterSize =
50+
model.size && model.activeParameters
51+
? tShared('modelParameters.totalAndActive', {
52+
total: model.size,
53+
active: model.activeParameters,
54+
})
55+
: model.size
56+
const hasContent = parameterSize || model.contextWindow || model.maxOutput || model.tokenPricing
3657
const currencyNames = new Intl.DisplayNames(locale, { type: 'currency' })
3758
const formatCurrencyName = (currency: string) => currencyNames.of(currency) ?? currency
3859

@@ -80,12 +101,12 @@ export function ModelSpecifications({ model }: ModelSpecificationsProps) {
80101
</h2>
81102

82103
<div className="grid grid-cols-1 md:grid-cols-2 gap-[var(--spacing-md)] mt-[var(--spacing-lg)]">
83-
{model.size && (
104+
{parameterSize && (
84105
<div className="border border-[var(--color-border)] p-[var(--spacing-md)]">
85106
<h3 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider font-medium mb-[var(--spacing-xs)]">
86107
{tShared('terms.modelSize')}
87108
</h3>
88-
<p className="text-lg font-semibold tracking-tight">{model.size}</p>
109+
<p className="text-lg font-semibold tracking-tight">{parameterSize}</p>
89110
</div>
90111
)}
91112

@@ -144,10 +165,13 @@ export function ModelSpecifications({ model }: ModelSpecificationsProps) {
144165
</span>
145166
)}
146167
</div>
147-
{offer.tiers.map((tier, index) => {
168+
{offer.tiers.map(tier => {
148169
const condition = formatCondition(tier.condition)
170+
const tierKey = tier.condition
171+
? `${tier.condition.metric}-${tier.condition.min ?? 'min'}-${tier.condition.max ?? 'max'}`
172+
: 'default'
149173
return (
150-
<div key={`${offer.id}-${index}`} className="space-y-1">
174+
<div key={`${offer.id}-${tierKey}`} className="space-y-1">
151175
{condition && (
152176
<p className="text-xs font-medium text-[var(--color-text-secondary)]">
153177
{condition}
@@ -182,6 +206,39 @@ export function ModelSpecifications({ model }: ModelSpecificationsProps) {
182206
)
183207
})}
184208
</div>
209+
) : model.referenceTokenPricing ? (
210+
<div className="space-y-[var(--spacing-sm)]">
211+
<p className="text-sm text-[var(--color-text-secondary)]">
212+
{tShared(reasonKeys[model.tokenPricing.reason])}
213+
</p>
214+
<div className="space-y-1 border-t border-[var(--color-border)] pt-[var(--spacing-sm)]">
215+
{rateRows.map(([rate, label]) => {
216+
const price = formatReferenceTokenRate(
217+
model.referenceTokenPricing!,
218+
rate,
219+
locale,
220+
conversion
221+
)
222+
if (!price) return null
223+
return (
224+
<p key={rate} className="text-sm">
225+
<span className="text-[var(--color-text-muted)] text-xs">{label} </span>
226+
<span className="font-semibold tracking-tight">
227+
{tShared('modelPricing.perMillionTokens', { price })}
228+
</span>
229+
</p>
230+
)
231+
})}
232+
<a
233+
href={model.referenceTokenPricing.source.url}
234+
target="_blank"
235+
rel="noopener noreferrer"
236+
className="inline-block text-xs text-[var(--color-text-muted)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
237+
>
238+
{model.referenceTokenPricing.source.name}
239+
</a>
240+
</div>
241+
</div>
185242
) : (
186243
<p className="text-sm text-[var(--color-text-secondary)]">
187244
{tShared(reasonKeys[model.tokenPricing.reason])}

src/components/product/VendorModels.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ export function VendorModels({ models }: { models: ManifestModel[] }) {
3636
<div className="flex flex-wrap gap-[var(--spacing-xs)] text-xs">
3737
{model.size && (
3838
<span className="px-2 py-0.5 bg-[var(--color-background-muted)] text-[var(--color-text-secondary)] border border-[var(--color-border)]">
39-
{model.size}
39+
{model.activeParameters
40+
? tShared('modelParameters.totalAndActive', {
41+
total: model.size,
42+
active: model.activeParameters,
43+
})
44+
: model.size}
4045
</span>
4146
)}
4247
<span className="px-2 py-0.5 bg-[var(--color-background-muted)] text-[var(--color-text-secondary)] border border-[var(--color-border)]">

0 commit comments

Comments
 (0)