Sfoglia il codice sorgente

feat(stats): use catalog pricing in efficiency and token costs

Adam 2 mesi fa
parent
commit
ba2455ecc7

+ 27 - 4
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 { 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)}`
 }
 }

+ 1 - 0
packages/stats/app/src/routes/index.css

@@ -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;

+ 29 - 4
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 { 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)}`
 }
 }

+ 77 - 5
packages/stats/app/src/routes/model-catalog.ts

@@ -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)
-    .then((response): Promise<unknown> => (response.ok ? (response.json() as Promise<unknown>) : Promise.resolve()))
-    .catch(() => undefined)
-  return buildModelCatalog(payload)
+  const [models, pricing] = await Promise.all([
+    fetchCatalogPayload(modelCatalogSourceUrl),
+    fetchCatalogPayload(modelCatalogPricingUrl),
+  ])
+  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)
 }
 }

+ 0 - 1
packages/stats/core/src/domain/home.ts

@@ -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,