@@ -462,7 +468,15 @@ function ModelEfficiencySection(props: { data: StatsModelData | null }) {
{(data) => (
-
+
= 10 ? 0 : 2)}`
}
+function formatCatalogPrice(value: ModelCatalogCost) {
+ return `${formatModelPrice(value.input)} / ${formatModelPrice(value.output)}`
+}
+
+function formatModelPrice(value: number) {
+ if (value > 0 && value < 0.01) return `$${value.toFixed(4)}`
+ return formatMoney(value)
+}
+
function formatSessionCost(value: number) {
return `$${value.toFixed(value > 0 && value < 0.01 ? 4 : 2)}`
}
diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css
index 8ae82963e..8c0277b1c 100644
--- a/packages/stats/app/src/routes/index.css
+++ b/packages/stats/app/src/routes/index.css
@@ -3843,6 +3843,7 @@
width: calc(100% + 48px);
margin-inline: -24px;
padding-inline: 24px;
+ padding-block-start: 2px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-padding-inline: 24px;
diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx
index 01cb109f7..2f5f6aa0d 100644
--- a/packages/stats/app/src/routes/index.tsx
+++ b/packages/stats/app/src/routes/index.tsx
@@ -27,6 +27,7 @@ import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show,
import { getRequestEvent } from "solid-js/web"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
+import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
import {
applyThemePreference,
Footer,
@@ -118,6 +119,7 @@ export default function StatsHome() {
)
const statsUnfurlUrl = new URL(statsUnfurlPath, statsHomeUrl).toString()
const data = createAsync(() => getData())
+ const catalog = createAsync(() => getModelCatalog())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal("system")
const updateThemePreference = (preference: ThemePreference) => {
@@ -168,7 +170,7 @@ export default function StatsHome() {
-
+
@@ -1446,10 +1448,10 @@ function marketDateParts(label: string) {
return { start: start ?? label, end: end ?? start ?? label }
}
-function TokenCostSection(props: { data: StatsHomeData["tokenCost"] }) {
+function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: ModelCatalog | null }) {
const [product, setProduct] = createSignal("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
- const data = createMemo(() => props.data[product()])
+ const data = createMemo(() => priceTokenCostFromCatalog(props.data[product()], props.catalog))
const visible = createMemo(() => data().slice(0, 13))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
@@ -1634,7 +1636,7 @@ function formatRatio(value: number) {
}
function formatDollars(value: number) {
- return `$${value.toFixed(2)}`
+ return `$${value.toFixed(value > 0 && value < 0.01 ? 4 : 2)}`
}
function MetricBar(props: { value: number; max: number; active: boolean }) {
@@ -1752,6 +1754,29 @@ function formatTokenCount(value: number) {
return `${Math.round(value / 1_000)}K`
}
+function priceTokenCostFromCatalog(data: TokenCostEntry[], catalog: ModelCatalog | null) {
+ if (!catalog) return data
+ return data
+ .flatMap((item) => {
+ const cost = catalogModelCost(catalog, item.model)
+ if (!cost) return []
+ return [
+ {
+ ...item,
+ total: cost.output,
+ input: cost.input,
+ output: cost.output,
+ cached: cost.cacheRead ?? cost.input,
+ },
+ ]
+ })
+ .toSorted((a, b) => a.total - b.total || a.model.localeCompare(b.model))
+}
+
+function catalogModelCost(catalog: ModelCatalog, model: string) {
+ return findModelCatalogEntry(catalog, model)?.cost
+}
+
function formatSessionCost(value: number) {
return `$${value.toFixed(4)}`
}
diff --git a/packages/stats/app/src/routes/model-catalog.ts b/packages/stats/app/src/routes/model-catalog.ts
index 8adddd39d..1eb1cda09 100644
--- a/packages/stats/app/src/routes/model-catalog.ts
+++ b/packages/stats/app/src/routes/model-catalog.ts
@@ -1,6 +1,14 @@
import { query } from "@solidjs/router"
export const modelCatalogSourceUrl = "https://models.dev/models.json"
+export const modelCatalogPricingUrl = "https://models.dev/api.json"
+
+export type ModelCatalogCost = {
+ input: number
+ output: number
+ cacheRead?: number
+ cacheWrite?: number
+}
export type ModelCatalogEntry = {
id: string
@@ -18,6 +26,7 @@ export type ModelCatalogEntry = {
toolCall: boolean
attachment: boolean
temperature: boolean
+ cost?: ModelCatalogCost
weights: { label: string; url: string }[]
benchmarks: ModelCatalogBenchmark[]
}
@@ -46,10 +55,11 @@ export type ModelCatalog = {
export const getModelCatalog = query(async () => {
"use server"
- const payload = await fetch(modelCatalogSourceUrl)
- .then((response): Promise => (response.ok ? (response.json() as Promise) : Promise.resolve()))
- .catch(() => undefined)
- return buildModelCatalog(payload)
+ const [models, pricing] = await Promise.all([
+ fetchCatalogPayload(modelCatalogSourceUrl),
+ fetchCatalogPayload(modelCatalogPricingUrl),
+ ])
+ return buildModelCatalog(models, pricing)
}, "getModelCatalog")
export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?: string) {
@@ -99,9 +109,18 @@ export function catalogSlug(value: string) {
.replace(/-{2,}/g, "-")
}
-function buildModelCatalog(payload: unknown): ModelCatalog {
+function buildModelCatalog(payload: unknown, pricingPayload?: unknown): ModelCatalog {
+ const costs = readCatalogCosts(pricingPayload)
const models = (Array.isArray(payload) ? payload : isRecord(payload) ? Object.values(payload) : [])
.flatMap(readModelCatalogEntry)
+ .map((model) => ({
+ ...model,
+ cost:
+ costs.get(catalogIdKey(model.id)) ??
+ costs.get(`${model.lab}/${model.slug}`) ??
+ costs.get(model.slug) ??
+ model.cost,
+ }))
.toSorted((a, b) => a.lab.localeCompare(b.lab) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate))
return {
models,
@@ -142,12 +161,61 @@ function readModelCatalogEntry(value: unknown): ModelCatalogEntry[] {
toolCall: booleanValue(value.tool_call),
attachment: booleanValue(value.attachment),
temperature: booleanValue(value.temperature),
+ cost: readCatalogCost(value.cost),
weights: readCatalogWeights(value.weights),
benchmarks: readCatalogBenchmarks(value.benchmarks),
},
]
}
+async function fetchCatalogPayload(url: string) {
+ return fetch(url)
+ .then((response): Promise => (response.ok ? (response.json() as Promise) : Promise.resolve()))
+ .catch(() => undefined)
+}
+
+function readCatalogCosts(payload: unknown) {
+ const costs = new Map()
+ const add = (model: unknown, provider?: string) => {
+ if (!isRecord(model)) return
+ const id = stringValue(model.id)
+ const cost = readCatalogCost(model.cost)
+ if (!id || !cost) return
+ costs.set(catalogIdKey(id), cost)
+ costs.set(catalogSlug(id), cost)
+ if (provider && !id.includes("/")) costs.set(`${catalogSlug(provider)}/${catalogSlug(id)}`, cost)
+ }
+
+ if (Array.isArray(payload)) {
+ payload.forEach((model) => add(model))
+ return costs
+ }
+ if (!isRecord(payload)) return costs
+
+ Object.entries(payload).forEach(([key, value]) => {
+ if (!isRecord(value)) return
+ if (isRecord(value.models)) {
+ Object.values(value.models).forEach((model) => add(model, stringValue(value.id) ?? key))
+ return
+ }
+ add(value)
+ })
+ return costs
+}
+
+function readCatalogCost(value: unknown): ModelCatalogCost | undefined {
+ if (!isRecord(value)) return undefined
+ const input = numberValue(value.input)
+ const output = numberValue(value.output)
+ if (input === undefined || output === undefined) return undefined
+ return {
+ input,
+ output,
+ cacheRead: numberValue(value.cache_read),
+ cacheWrite: numberValue(value.cache_write),
+ }
+}
+
function readCatalogLimit(value: unknown) {
if (!isRecord(value)) return undefined
return {
@@ -201,6 +269,10 @@ function displayDateTime(value: string | undefined) {
return value ? new Date(value).getTime() || 0 : 0
}
+function catalogIdKey(value: string) {
+ return value.split("/").map(catalogSlug).join("/")
+}
+
function isRecord(value: unknown): value is Record {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
diff --git a/packages/stats/core/src/domain/home.ts b/packages/stats/core/src/domain/home.ts
index 1bdb11355..c9c9c4b28 100644
--- a/packages/stats/core/src/domain/home.ts
+++ b/packages/stats/core/src/domain/home.ts
@@ -427,7 +427,6 @@ function buildTokenCost(rows: StatMetricRow[], product: TokenProduct, window: Da
return topModelsByUsage(rows, product, window)
.flatMap((item) => {
const total = costPerMillion(item.totalCostMicrocents, item.totalTokens)
- if (total === 0) return []
return [
{
model: item.model,