diff --git a/packages/stats/app/src/routes/[lab]/index.tsx b/packages/stats/app/src/routes/[lab]/index.tsx index f46b2ed0ac8..5567700341a 100644 --- a/packages/stats/app/src/routes/[lab]/index.tsx +++ b/packages/stats/app/src/routes/[lab]/index.tsx @@ -469,6 +469,12 @@ function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | nul {formatTokens(active.point.tokens)}
++ + {i18n.t("model.uniqueUsers")} + + {formatUsers(active.point.users)} +
) }} @@ -759,7 +765,7 @@ function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) { } function labRelatedDescription(lab: ModelCatalogLab) { - return lab.description ?? `OpenCode Go usage across ${lab.name} models, recent token volume, and model limits.` + return lab.description ?? "" } function relatedTone(share: number) { diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css index b4099a55ed6..9fcd67af174 100644 --- a/packages/stats/app/src/routes/index.css +++ b/packages/stats/app/src/routes/index.css @@ -4218,7 +4218,7 @@ [data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] { z-index: 6; - top: clamp(12px, calc(var(--lab-usage-user-y) * 1% - 38px), calc(100% - 92px)); + top: clamp(12px, calc(var(--lab-usage-user-y) * 1% - 46px), calc(100% - 108px)); width: 192px; min-width: 192px; background: var(--stats-bg); @@ -4240,7 +4240,7 @@ [data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] p { height: 16px; - margin: 8px 0; + margin: 4px 0; } [data-page="stats"] @@ -4250,6 +4250,13 @@ background: var(--lab-usage-active); } +[data-page="stats"] + [data-component="model-usage-chart"][data-variant="lab-usage"] + [data-component="chart-tooltip"] + i[data-kind="users"] { + background: var(--lab-usage-line); +} + [data-page="stats"] [data-component="model-peer-list"] { display: grid; gap: 8px; diff --git a/packages/stats/app/src/routes/model-catalog.ts b/packages/stats/app/src/routes/model-catalog.ts index 68e0c012da9..18783e96d48 100644 --- a/packages/stats/app/src/routes/model-catalog.ts +++ b/packages/stats/app/src/routes/model-catalog.ts @@ -2,6 +2,7 @@ import { query } from "@solidjs/router" export const modelCatalogSourceUrl = "https://models.dev/catalog.json" export const modelCatalogPricingUrl = "https://models.dev/api.json" +export const modelCatalogLabSourceUrl = "https://models.dev/labs" export type ModelCatalogCost = { input: number @@ -57,11 +58,12 @@ export type ModelCatalog = { export const getModelCatalog = query(async () => { "use server" - const [models, pricing] = await Promise.all([ + const [models, pricing, labs] = await Promise.all([ fetchCatalogPayload(modelCatalogSourceUrl), fetchCatalogPayload(modelCatalogPricingUrl), + fetchLabCatalogPayload(modelCatalogLabSourceUrl), ]) - return buildModelCatalog(models, pricing) + return buildModelCatalog(models, pricing, labs) }, "getModelCatalog") export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?: string) { @@ -113,9 +115,9 @@ export function catalogSlug(value: string) { .replace(/-{2,}/g, "-") } -function buildModelCatalog(payload: unknown, pricingPayload?: unknown): ModelCatalog { +function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayload?: unknown): ModelCatalog { const costs = readCatalogCosts(pricingPayload) - const labDescriptions = readCatalogLabDescriptions(payload, pricingPayload) + const labDescriptions = readCatalogLabDescriptions(payload, pricingPayload, labPayload) const models = readCatalogModels(payload) .flatMap(readModelCatalogEntry) .map((model) => ({ @@ -191,7 +193,8 @@ function readCatalogLabDescriptions(...payloads: unknown[]) { if (!description) return const id = stringValue(value.id) ?? fallbackId const name = stringValue(value.name) - const keys = [id, name] + const title = stringValue(value.title) + const keys = [id, name, title] keys.forEach((key) => { if (!key) return descriptions.set(catalogLabSlug(key), description) @@ -226,6 +229,21 @@ async function fetchCatalogPayload(url: string) { .catch(() => undefined) } +async function fetchLabCatalogPayload(url: string) { + return fetch(url) + .then((response) => (response.ok ? response.text() : Promise.resolve(""))) + .then(readLabSearchIndex) + .catch(() => undefined) +} + +function readLabSearchIndex(html: string) { + const match = /