diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index 825cfc1b0..95fbd30c0 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -20,7 +20,13 @@ import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js import { getRequestEvent } from "solid-js/web" import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson" import type { GeometryCollection, Topology } from "topojson-specification" -import { findModelCatalogEntry, formatCatalogLabName, getModelCatalog, type ModelCatalogEntry } from "../model-catalog" +import { + findModelCatalogEntry, + formatCatalogLabName, + getModelCatalog, + type ModelCatalogCost, + type ModelCatalogEntry, +} from "../model-catalog" import { applyThemePreference, Footer, @@ -169,7 +175,7 @@ export default function StatsModel() { - + @@ -449,7 +455,7 @@ function ModelUsageSection(props: { data: ModelUsagePoint[] }) { ) } -function ModelEfficiencySection(props: { data: StatsModelData | null }) { +function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) { return (
@@ -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,