feat(stats): use catalog pricing in efficiency and token costs
This commit is contained in:
parent
44308dfd7e
commit
ba2455ecc7
5 changed files with 134 additions and 14 deletions
|
|
@ -20,7 +20,13 @@ import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
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 {
|
import {
|
||||||
applyThemePreference,
|
applyThemePreference,
|
||||||
Footer,
|
Footer,
|
||||||
|
|
@ -169,7 +175,7 @@ export default function StatsModel() {
|
||||||
<ModelHero data={stats() ?? null} catalog={catalogEntry() ?? null} labName={labName()} />
|
<ModelHero data={stats() ?? null} catalog={catalogEntry() ?? null} labName={labName()} />
|
||||||
<ModelOverview data={stats() ?? null} />
|
<ModelOverview data={stats() ?? null} />
|
||||||
<ModelUsageSection data={stats()?.usage ?? []} />
|
<ModelUsageSection data={stats()?.usage ?? []} />
|
||||||
<ModelEfficiencySection data={stats() ?? null} />
|
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||||
<ModelPeersSection data={stats() ?? null} />
|
<ModelPeersSection data={stats() ?? null} />
|
||||||
</>
|
</>
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<section id="efficiency" data-section="model-panel">
|
<section id="efficiency" data-section="model-panel">
|
||||||
<SectionTitle title="Efficiency" description="Cost, cache behavior, and average session shape." />
|
<SectionTitle title="Efficiency" description="Cost, cache behavior, and average session shape." />
|
||||||
|
|
@ -462,7 +468,15 @@ function ModelEfficiencySection(props: { data: StatsModelData | null }) {
|
||||||
{(data) => (
|
{(data) => (
|
||||||
<div data-component="model-metric-grid" data-variant="dense">
|
<div data-component="model-metric-grid" data-variant="dense">
|
||||||
<MetricCard label="Cost" value={formatMoney(data().totals.cost)} detail="total spend" />
|
<MetricCard label="Cost" value={formatMoney(data().totals.cost)} detail="total spend" />
|
||||||
<MetricCard label="Cost / 1M" value={formatMoney(data().totals.costPerMillion)} detail="all tokens" />
|
<MetricCard
|
||||||
|
label="Cost / 1M"
|
||||||
|
value={
|
||||||
|
props.catalog?.cost
|
||||||
|
? formatCatalogPrice(props.catalog.cost)
|
||||||
|
: formatMoney(data().totals.costPerMillion)
|
||||||
|
}
|
||||||
|
detail={props.catalog?.cost ? "input / output" : "observed all tokens"}
|
||||||
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Cost / Session"
|
label="Cost / Session"
|
||||||
value={formatSessionCost(data().totals.costPerSession)}
|
value={formatSessionCost(data().totals.costPerSession)}
|
||||||
|
|
@ -784,6 +798,15 @@ function formatMoney(value: number) {
|
||||||
return `$${value.toFixed(value >= 10 ? 0 : 2)}`
|
return `$${value.toFixed(value >= 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) {
|
function formatSessionCost(value: number) {
|
||||||
return `$${value.toFixed(value > 0 && value < 0.01 ? 4 : 2)}`
|
return `$${value.toFixed(value > 0 && value < 0.01 ? 4 : 2)}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3843,6 +3843,7 @@
|
||||||
width: calc(100% + 48px);
|
width: calc(100% + 48px);
|
||||||
margin-inline: -24px;
|
margin-inline: -24px;
|
||||||
padding-inline: 24px;
|
padding-inline: 24px;
|
||||||
|
padding-block-start: 2px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overscroll-behavior-x: contain;
|
overscroll-behavior-x: contain;
|
||||||
scroll-padding-inline: 24px;
|
scroll-padding-inline: 24px;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show,
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||||
|
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
|
||||||
import {
|
import {
|
||||||
applyThemePreference,
|
applyThemePreference,
|
||||||
Footer,
|
Footer,
|
||||||
|
|
@ -118,6 +119,7 @@ export default function StatsHome() {
|
||||||
)
|
)
|
||||||
const statsUnfurlUrl = new URL(statsUnfurlPath, statsHomeUrl).toString()
|
const statsUnfurlUrl = new URL(statsUnfurlPath, statsHomeUrl).toString()
|
||||||
const data = createAsync(() => getData())
|
const data = createAsync(() => getData())
|
||||||
|
const catalog = createAsync(() => getModelCatalog())
|
||||||
const githubStars = createAsync(() => getGitHubStars())
|
const githubStars = createAsync(() => getGitHubStars())
|
||||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||||
const updateThemePreference = (preference: ThemePreference) => {
|
const updateThemePreference = (preference: ThemePreference) => {
|
||||||
|
|
@ -168,7 +170,7 @@ export default function StatsHome() {
|
||||||
<Hero updatedAt={stats().updatedAt} />
|
<Hero updatedAt={stats().updatedAt} />
|
||||||
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
|
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
|
||||||
<SessionCostSection data={stats().sessionCost} />
|
<SessionCostSection data={stats().sessionCost} />
|
||||||
<TokenCostSection data={stats().tokenCost} />
|
<TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
|
||||||
<CacheRatioSection data={stats().cacheRatio} />
|
<CacheRatioSection data={stats().cacheRatio} />
|
||||||
<MarketShareSection data={stats().market} />
|
<MarketShareSection data={stats().market} />
|
||||||
<GeoBreakdownSection data={stats().country} />
|
<GeoBreakdownSection data={stats().country} />
|
||||||
|
|
@ -1446,10 +1448,10 @@ function marketDateParts(label: string) {
|
||||||
return { start: start ?? label, end: end ?? start ?? label }
|
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<TokenProduct>("Go")
|
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
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 visible = createMemo(() => data().slice(0, 13))
|
||||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||||
|
|
||||||
|
|
@ -1634,7 +1636,7 @@ function formatRatio(value: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDollars(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 }) {
|
function MetricBar(props: { value: number; max: number; active: boolean }) {
|
||||||
|
|
@ -1752,6 +1754,29 @@ function formatTokenCount(value: number) {
|
||||||
return `${Math.round(value / 1_000)}K`
|
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) {
|
function formatSessionCost(value: number) {
|
||||||
return `$${value.toFixed(4)}`
|
return `$${value.toFixed(4)}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import { query } from "@solidjs/router"
|
import { query } from "@solidjs/router"
|
||||||
|
|
||||||
export const modelCatalogSourceUrl = "https://models.dev/models.json"
|
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 = {
|
export type ModelCatalogEntry = {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -18,6 +26,7 @@ export type ModelCatalogEntry = {
|
||||||
toolCall: boolean
|
toolCall: boolean
|
||||||
attachment: boolean
|
attachment: boolean
|
||||||
temperature: boolean
|
temperature: boolean
|
||||||
|
cost?: ModelCatalogCost
|
||||||
weights: { label: string; url: string }[]
|
weights: { label: string; url: string }[]
|
||||||
benchmarks: ModelCatalogBenchmark[]
|
benchmarks: ModelCatalogBenchmark[]
|
||||||
}
|
}
|
||||||
|
|
@ -46,10 +55,11 @@ export type ModelCatalog = {
|
||||||
|
|
||||||
export const getModelCatalog = query(async () => {
|
export const getModelCatalog = query(async () => {
|
||||||
"use server"
|
"use server"
|
||||||
const payload = await fetch(modelCatalogSourceUrl)
|
const [models, pricing] = await Promise.all([
|
||||||
.then((response): Promise<unknown> => (response.ok ? (response.json() as Promise<unknown>) : Promise.resolve()))
|
fetchCatalogPayload(modelCatalogSourceUrl),
|
||||||
.catch(() => undefined)
|
fetchCatalogPayload(modelCatalogPricingUrl),
|
||||||
return buildModelCatalog(payload)
|
])
|
||||||
|
return buildModelCatalog(models, pricing)
|
||||||
}, "getModelCatalog")
|
}, "getModelCatalog")
|
||||||
|
|
||||||
export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?: string) {
|
export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?: string) {
|
||||||
|
|
@ -99,9 +109,18 @@ export function catalogSlug(value: string) {
|
||||||
.replace(/-{2,}/g, "-")
|
.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) : [])
|
const models = (Array.isArray(payload) ? payload : isRecord(payload) ? Object.values(payload) : [])
|
||||||
.flatMap(readModelCatalogEntry)
|
.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))
|
.toSorted((a, b) => a.lab.localeCompare(b.lab) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate))
|
||||||
return {
|
return {
|
||||||
models,
|
models,
|
||||||
|
|
@ -142,12 +161,61 @@ function readModelCatalogEntry(value: unknown): ModelCatalogEntry[] {
|
||||||
toolCall: booleanValue(value.tool_call),
|
toolCall: booleanValue(value.tool_call),
|
||||||
attachment: booleanValue(value.attachment),
|
attachment: booleanValue(value.attachment),
|
||||||
temperature: booleanValue(value.temperature),
|
temperature: booleanValue(value.temperature),
|
||||||
|
cost: readCatalogCost(value.cost),
|
||||||
weights: readCatalogWeights(value.weights),
|
weights: readCatalogWeights(value.weights),
|
||||||
benchmarks: readCatalogBenchmarks(value.benchmarks),
|
benchmarks: readCatalogBenchmarks(value.benchmarks),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchCatalogPayload(url: string) {
|
||||||
|
return fetch(url)
|
||||||
|
.then((response): Promise<unknown> => (response.ok ? (response.json() as Promise<unknown>) : Promise.resolve()))
|
||||||
|
.catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCatalogCosts(payload: unknown) {
|
||||||
|
const costs = new Map<string, ModelCatalogCost>()
|
||||||
|
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) {
|
function readCatalogLimit(value: unknown) {
|
||||||
if (!isRecord(value)) return undefined
|
if (!isRecord(value)) return undefined
|
||||||
return {
|
return {
|
||||||
|
|
@ -201,6 +269,10 @@ function displayDateTime(value: string | undefined) {
|
||||||
return value ? new Date(value).getTime() || 0 : 0
|
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<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,6 @@ function buildTokenCost(rows: StatMetricRow[], product: TokenProduct, window: Da
|
||||||
return topModelsByUsage(rows, product, window)
|
return topModelsByUsage(rows, product, window)
|
||||||
.flatMap((item) => {
|
.flatMap((item) => {
|
||||||
const total = costPerMillion(item.totalCostMicrocents, item.totalTokens)
|
const total = costPerMillion(item.totalCostMicrocents, item.totalTokens)
|
||||||
if (total === 0) return []
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
model: item.model,
|
model: item.model,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue