Compare commits

..

8 Commits

Author SHA1 Message Date
opencode-agent[bot] b1f8cc04af chore: update nix node_modules hashes 2026-08-05 20:33:25 +00:00
opencode-agent[bot] 082fe93e16 chore: generate 2026-08-05 20:16:40 +00:00
Aiden Cline 709c195905 fix(opencode): preserve compatible stream errors (#40718) 2026-08-05 15:14:03 -05:00
opencode-agent[bot] 3355b78d91 chore: generate 2026-08-05 20:06:05 +00:00
Adam b84c63d034 fix(stats): reduce html payloads 2026-08-05 15:02:39 -05:00
opencode-agent[bot] 057b5a9dee chore: generate 2026-08-05 19:41:32 +00:00
Aiden Cline 61aefc0759 fix(opencode): expand retryable error patterns (#40707) 2026-08-05 14:39:49 -05:00
Aiden Cline f929f8f100 refactor(opencode): simplify retry error matching (#40694) 2026-08-05 13:05:19 -05:00
12 changed files with 450 additions and 551 deletions
+1
View File
@@ -1078,6 +1078,7 @@
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
},
"overrides": {
"@opentui/core": "catalog:",
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=",
"aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=",
"aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=",
"x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY="
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
}
}
+2 -1
View File
@@ -158,6 +158,7 @@
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch"
}
}
+19 -7
View File
@@ -28,11 +28,13 @@ export const RETRY_BACKOFF_FACTOR = 2
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
const RETRYABLE_MESSAGE = [
/\b(?:server[_\s-]?error|internal[_\s-]?error|service[_\s-]?unavailable|overloaded|too many requests|rate increased too quickly|provider[_\s-]?returned[_\s-]?error)\b|\brate[_\s-]?limit/i,
/\b(?:fetch failed|network error|upstream connect|connection (?:error|refused|lost)|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again)\b|^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed? out)\b/i,
/\b(?:resource[_\s-]?exhausted|please retry your request|you can retry your request|try your request again)\b/i,
/\b(?:429|500|502|503|504|524)\b/,
const RETRYABLE_MESSAGE_PATTERNS = [
/429|500|502|503|504|524/i,
/rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i,
/overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i,
/terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i,
/^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i,
/try your request again|retry your request|resource exhausted|resource_exhausted/i,
]
function cap(ms: number) {
@@ -79,7 +81,13 @@ export function retryable(error: Err, provider: string) {
const status = error.data.statusCode
// 5xx errors are transient server failures and should always be retried,
// even when the provider SDK doesn't explicitly mark them as retryable.
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
if (
!error.data.isRetryable &&
!(status !== undefined && status >= 500) &&
!matchesRetryableMessage(error.data.message) &&
!matchesRetryableMessage(error.data.responseBody)
)
return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError")) {
return {
message: GO_UPSELL_MESSAGE,
@@ -134,10 +142,14 @@ export function retryable(error: Err, provider: string) {
const lower = message.toLowerCase()
if (lower.includes("too_many_requests")) return { message: "Too Many Requests" }
if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" }
if (RETRYABLE_MESSAGE.some((pattern) => pattern.test(message))) return { message }
if (matchesRetryableMessage(message)) return { message }
return undefined
}
function matchesRetryableMessage(value: unknown) {
return typeof value === "string" && RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(value))
}
function str(value: unknown) {
if (value === undefined || value === null) return ""
return String(value)
@@ -604,6 +604,53 @@ it.live("session.processor effect tests retry recognized structured json errors"
),
)
it.live("session.processor effect tests retry OpenAI-compatible midstream server errors", () =>
provideTmpdirServer(
({ dir, llm }) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()
yield* llm.push(raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] }))
yield* llm.text("after")
const chat = yield* session.create({})
const parent = yield* user(chat.id, "retry midstream server error")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const handle = yield* processors.create({
assistantMessage: msg,
sessionID: chat.id,
model: mdl,
})
const value = yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "retry midstream server error" }],
tools: {},
})
const parts = yield* MessageV2.parts(msg.id)
expect(value).toBe("continue")
expect(yield* llm.calls).toBe(2)
expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
expect(handle.message.error).toBeUndefined()
}),
{ config: (url) => providerCfg(url) },
),
)
it.live("session.processor effect tests publish retry status updates", () =>
provideTmpdirServer(
({ dir, llm }) =>
@@ -170,17 +170,43 @@ describe("session.retry.retryable", () => {
test.each([
"Internal server error",
"internal error",
"server-error",
"Provider returned error",
"provider-returned-error",
"terminated",
"fetch failed",
"connection refused",
"connect ECONNREFUSED",
"request ETIMEDOUT",
"failed to fetch",
"EAI_AGAIN",
"response timed out",
"Please retry your request",
"try your request again",
"upstream returned status 524",
])("retries matching API error text: %s", (message) => {
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
})
test("retries hyphenated service-unavailable errors", () => {
expect(SessionRetry.retryable(wrap("service-unavailable"), retryProvider)).toEqual({
message: "Provider is overloaded",
})
})
test("matches retryable API response bodies", () => {
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Request failed",
isRetryable: false,
statusCode: 400,
responseBody: JSON.stringify({ error: { message: "upstream connection refused" } }),
}).toObject(),
)
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Request failed" })
})
test("retries transport timeout errors", () => {
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
expect(SessionV1.APIError.isInstance(request)).toBe(true)
+74 -124
View File
@@ -1,34 +1,23 @@
import { Meta, Title } from "@solidjs/meta"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { geoEquirectangular, geoPath } from "d3-geo"
import { scaleSqrt } from "d3-scale"
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
import {
getStatsModelData,
type CountryEntry,
type ModelPeerEntry,
type ModelUsagePoint,
type StatsModelData,
type UsageRange,
} from "@opencode-ai/stats-core/domain/home"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, createSignal, createUniqueId, 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 { LocaleLinks } from "../../component/locale-links"
import { useI18n } from "../../context/i18n"
import { useLanguage } from "../../context/language"
import { localizedUrl } from "../../lib/language"
import {
findModelCatalogEntry,
formatCatalogLabName,
getModelCatalog,
type ModelCatalog,
type ModelCatalogEntry,
} from "../model-catalog"
import { findModelCatalogEntry, formatCatalogLabName, loadModelCatalog, type ModelCatalogEntry } from "../model-catalog"
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map"
import { SectionHeading } from "../section-heading"
import { runStatsEffect } from "../../stats-runtime"
import { setStatsPageCacheHeaders } from "../stats-cache"
@@ -51,45 +40,41 @@ import {
} from "../stats-shell"
const statsUnfurlPath = "banner.png"
const geoMapWidth = 960
const geoMapHeight = 430
const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
type IsoCountryCode = readonly [string, string, string]
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
type ModelCatalogOption = Pick<ModelCatalogEntry, "id" | "lab" | "slug" | "name">
type ModelPageCatalog = {
entry: ModelCatalogEntry | null
labs: { id: string; name: string }[]
labModels: ModelCatalogOption[]
}
type StatsModelPageData = Omit<StatsModelData, "country"> & { country: CountryEntry[] }
type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null }
const countryNumericIds = new Map(
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
)
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
marker: geoCountryMarker(country),
}))
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
const getModelData = query(async (lab: string, model: string) => {
const getModelPageData = query(async (labParam: string, modelParam: string) => {
"use server"
return runStatsEffect(getStatsModelData(model, lab))
}, "getStatsModelData")
const catalog = await loadModelCatalog()
const entry = findModelCatalogEntry(catalog, modelParam, labParam) ?? null
const lab = entry?.lab ?? labParam
const model = entry?.slug ?? modelParam
const stats = lab && model ? await runStatsEffect(getStatsModelData(model, lab)) : null
return {
catalog: {
entry,
labs: catalog.labs.map((item) => ({ id: item.id, name: item.name })),
labModels:
catalog.labs
.find((item) => item.id === (entry?.lab ?? providerSlug(labParam)))
?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [],
},
stats: stats ? { ...stats, country: stats.country["2M"] } : null,
} satisfies ModelPageData
}, "getStatsModelPageData")
export default function StatsModel() {
const i18n = useI18n()
@@ -99,18 +84,9 @@ export default function StatsModel() {
const params = useParams()
const labParam = createMemo(() => params.lab ?? "")
const modelParam = createMemo(() => params.model ?? "")
const catalog = createAsync(() => getModelCatalog())
const catalogEntry = createMemo(() => {
const data = catalog()
if (!data) return undefined
return findModelCatalogEntry(data, modelParam(), labParam()) ?? null
})
const stats = createAsync(() => {
const entry = catalogEntry()
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
if (!entry && (!labParam() || !modelParam())) return Promise.resolve(null)
return getModelData(labParam(), entry?.slug ?? modelParam())
})
const page = createAsync(() => getModelPageData(labParam(), modelParam()))
const catalogEntry = createMemo(() => page()?.catalog.entry)
const stats = createMemo(() => page()?.stats)
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? i18n.t("model.fallback"))
@@ -179,13 +155,13 @@ export default function StatsModel() {
<Header githubStars={githubStars() ?? "150K"} links={modelHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
<div data-component="container">
<div data-component="content">
<Show when={catalogEntry() || stats() !== undefined} fallback={<ModelLoading />}>
<Show when={page() !== undefined} fallback={<ModelLoading />}>
<Show when={catalogEntry() || stats()} fallback={<ModelNotFound lab={labParam()} model={modelParam()} />}>
<>
<ModelHero
data={stats() ?? null}
catalog={catalogEntry() ?? null}
catalogData={catalog() ?? null}
catalogData={page()?.catalog ?? null}
labName={labName()}
/>
<ModelOverview catalog={catalogEntry() ?? null} />
@@ -193,10 +169,10 @@ export default function StatsModel() {
<ModelUsageSection data={stats() ?? null} />
<ModelUniqueUsersSection data={stats() ?? null} />
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
<ModelGeoBreakdownSection data={stats()?.country ?? []} />
<ModelPeersSection data={stats() ?? null} />
<ComparisonCardsSection
pairs={modelComparisonPairs(catalog(), catalogEntry() ?? null, stats() ?? null)}
pairs={modelComparisonPairs(page()?.catalog.labModels, catalogEntry() ?? null, stats() ?? null)}
title="Compare This Model"
description="Other models to compare with this one."
variant="featured"
@@ -271,9 +247,9 @@ function ModelNotFound(props: { lab: string; model: string }) {
}
function ModelHero(props: {
data: StatsModelData | null
data: StatsModelPageData | null
catalog: ModelCatalogEntry | null
catalogData: ModelCatalog | null
catalogData: ModelPageCatalog | null
labName: string
}) {
const i18n = useI18n()
@@ -282,9 +258,7 @@ function ModelHero(props: {
const modelName = () => props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback")
const weights = () => props.catalog?.weights[0]
const labs = () => props.catalogData?.labs ?? []
const labModels = () =>
props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ??
(props.catalog ? [props.catalog] : [])
const labModels = () => props.catalogData?.labModels ?? (props.catalog ? [props.catalog] : [])
return (
<section id="overview" data-section="model-hero">
<nav data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
@@ -403,7 +377,7 @@ function ModelHeroActionIcon(props: { kind: "weights" | "compare" }) {
)
}
function ModelHeroSparkline(props: { data: StatsModelData }) {
function ModelHeroSparkline(props: { data: StatsModelPageData }) {
const values = () => props.data.usage.slice(-14).map((point) => point.tokens)
return (
<span data-slot="model-hero-sparkline" aria-hidden="true">
@@ -466,7 +440,7 @@ function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
)
}
function ModelMomentumSection(props: { data: StatsModelData | null }) {
function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
const i18n = useI18n()
const language = useLanguage()
return (
@@ -505,7 +479,7 @@ function ModelMomentumSection(props: { data: StatsModelData | null }) {
)
}
function MomentumChart(props: { data: StatsModelData; locale: string }) {
function MomentumChart(props: { data: StatsModelPageData; locale: string }) {
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
return (
@@ -562,7 +536,7 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
)
}
function ModelUsageSection(props: { data: StatsModelData | null }) {
function ModelUsageSection(props: { data: StatsModelPageData | null }) {
const i18n = useI18n()
return (
<ModelTrendSection
@@ -582,7 +556,7 @@ function ModelUsageSection(props: { data: StatsModelData | null }) {
)
}
function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
function ModelUniqueUsersSection(props: { data: StatsModelPageData | null }) {
const i18n = useI18n()
return (
<ModelTrendSection
@@ -606,7 +580,7 @@ function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
}
function ModelTrendSection(props: {
data: StatsModelData | null
data: StatsModelPageData | null
id: string
title: string
description: string
@@ -859,7 +833,7 @@ function ModelTrendSection(props: {
)
}
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalog: ModelCatalogEntry | null }) {
const i18n = useI18n()
return (
<section id="efficiency" data-section="model-panel">
@@ -910,11 +884,11 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M
)
}
function ModelGeoBreakdownSection(props: { data: Record<UsageRange, CountryEntry[]> }) {
function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
const i18n = useI18n()
const language = useLanguage()
const [activeCountry, setActiveCountry] = createSignal<string>()
const data = createMemo(() => props.data["2M"])
const data = createMemo(() => props.data)
const countryById = createMemo(
() =>
new Map(
@@ -1031,31 +1005,30 @@ function GeoWorldMap(props: {
</For>
</g>
<g data-slot="geo-country-markers">
<For each={worldCountryPaths}>
<For each={worldCountryMarkers}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<Show when={country.marker && entry() ? country.marker : undefined}>
{(marker) => (
<circle
cx={marker().x}
cy={marker().y}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
)}
<Show when={entry()}>
<circle
cx={country.marker.x}
cy={country.marker.y}
data-country-id={country.id}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
</Show>
)
}}
@@ -1103,7 +1076,7 @@ function GeoCountryList(props: {
)
}
function ModelPeersSection(props: { data: StatsModelData | null }) {
function ModelPeersSection(props: { data: StatsModelPageData | null }) {
const i18n = useI18n()
return (
<section id="peers" data-section="model-panel">
@@ -1172,9 +1145,9 @@ function ModelEmptyState(props: { title: string; description: string; compact?:
}
function modelComparisonPairs(
catalog: ModelCatalog | undefined,
catalogModels: ModelCatalogOption[] | undefined,
catalogEntry: ModelCatalogEntry | null,
data: StatsModelData | null,
data: StatsModelPageData | null,
) {
const current = modelComparisonRef(catalogEntry, data)
if (!current) return []
@@ -1192,9 +1165,7 @@ function modelComparisonPairs(
},
detail: "Usage peer",
}))
const catalogPairs = (
catalogEntry && catalog ? (catalog.labs.find((lab) => lab.id === catalogEntry.lab)?.models ?? []) : []
)
const catalogPairs = (catalogEntry ? (catalogModels ?? []) : [])
.filter((model) => model.id !== catalogEntry?.id)
.slice(0, 3)
.map((model) => ({
@@ -1207,7 +1178,7 @@ function modelComparisonPairs(
function modelComparisonRef(
catalogEntry: ModelCatalogEntry | null,
data: StatsModelData | null,
data: StatsModelPageData | null,
): ComparisonModelRef | undefined {
if (catalogEntry) return modelRefFromCatalog(catalogEntry)
if (!data) return undefined
@@ -1227,31 +1198,10 @@ function getProviderIconId(author: string) {
return author.toLowerCase().replace(/[^a-z0-9]+/g, "")
}
function emptyCountryRecord(): Record<UsageRange, CountryEntry[]> {
return {
"1D": [],
"1W": [],
"2W": [],
"1M": [],
"2M": [],
"3M": [],
YTD: [],
ALL: [],
}
}
function countryNumericId(country: string) {
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
}
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
function formatCountryName(country: string, locale: string, i18n: ReturnType<typeof useI18n>) {
const code = country.toUpperCase()
if (code === "ZZ") return i18n.t("home.unknown")
@@ -1508,7 +1458,7 @@ function formatSparklinePoint(value: number) {
return Number(value.toFixed(2)).toString()
}
function formatModelRankMoveLabel(data: StatsModelData, i18n: ReturnType<typeof useI18n>) {
function formatModelRankMoveLabel(data: StatsModelPageData, i18n: ReturnType<typeof useI18n>) {
if (data.rank === null) return i18n.t("model.noUsageLastWeek")
if (data.previousRank === null) return i18n.t("model.newThisWeek")
const change = data.previousRank - data.rank
+42 -39
View File
@@ -6,7 +6,6 @@ import {
type LabUsageModelEntry,
type MarketDay,
type ModelUsagePoint,
type StatsHomeData,
type StatsLabData,
} from "@opencode-ai/stats-core/domain/home"
import { createAsync, query, useParams } from "@solidjs/router"
@@ -20,7 +19,7 @@ import {
catalogSlug,
findModelCatalogLab,
formatCatalogLabName,
getModelCatalog,
loadModelCatalog,
type ModelCatalogEntry,
type ModelCatalogLab,
} from "../model-catalog"
@@ -42,15 +41,33 @@ import {
const statsUnfurlPath = "banner.png"
const getLabData = query(async (lab: string) => {
"use server"
return runStatsEffect(getStatsLabData(lab))
}, "getStatsLabData")
type RelatedCatalogLab = Pick<ModelCatalogLab, "id" | "name" | "description"> & {
models: Pick<ModelCatalogEntry, "name">[]
}
const getHomeData = query(async () => {
type LabPageData = {
lab: ModelCatalogLab | null
labs: RelatedCatalogLab[]
market: MarketDay[]
stats: StatsLabData | null
}
const getLabPageData = query(async (labParam: string) => {
"use server"
return runStatsEffect(getStatsHomeData())
}, "getStatsHomeData")
const [catalog, home] = await Promise.all([loadModelCatalog(), runStatsEffect(getStatsHomeData())])
const lab = findModelCatalogLab(catalog, labParam) ?? null
return {
lab,
labs: catalog.labs.map((entry) => ({
id: entry.id,
name: entry.name,
description: entry.description,
models: entry.models.map((model) => ({ name: model.name })),
})),
market: home.market["2M"],
stats: lab ? await runStatsEffect(getStatsLabData(lab.id)) : null,
} satisfies LabPageData
}, "getStatsLabPageData")
type LabModelTooltipState = {
model: ModelCatalogEntry
@@ -67,19 +84,9 @@ export default function StatsLab() {
setStatsPageCacheHeaders(event?.response.headers)
const params = useParams()
const labParam = createMemo(() => params.lab ?? "")
const catalog = createAsync(() => getModelCatalog())
const lab = createMemo(() => {
const data = catalog()
if (!data) return undefined
return findModelCatalogLab(data, labParam()) ?? null
})
const stats = createAsync(() => {
const entry = lab()
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
if (!entry) return Promise.resolve(null)
return getLabData(entry.id)
})
const homeStats = createAsync((): Promise<StatsHomeData | undefined> => getHomeData())
const page = createAsync(() => getLabPageData(labParam()))
const lab = createMemo(() => page()?.lab)
const stats = createMemo(() => page()?.stats)
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const labName = createMemo(() => lab()?.name ?? formatCatalogLabName(labParam()))
@@ -137,19 +144,15 @@ export default function StatsLab() {
<Header githubStars={githubStars() ?? "150K"} links={labHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
<div data-component="container">
<div data-component="content">
<Show when={catalog() !== undefined} fallback={<LabLoading />}>
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={catalog()?.labs ?? []} />}>
<Show when={page() !== undefined} fallback={<LabLoading />}>
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={page()?.labs ?? []} />}>
{(data) => (
<>
<LabHero lab={data()} labs={catalog()?.labs ?? []} />
<LabHero lab={data()} labs={page()?.labs ?? []} />
<LabOverview lab={data()} data={stats() ?? null} />
<LabUsageSection lab={data()} data={stats() ?? null} />
<LabModelsSection lab={data()} usage={stats()?.models ?? []} />
<LabRelatedSection
lab={data()}
labs={catalog()?.labs ?? []}
market={homeStats()?.market["2M"] ?? []}
/>
<LabRelatedSection lab={data()} labs={page()?.labs ?? []} market={page()?.market ?? []} />
<ComparisonCardsSection
pairs={labComparisonPairs(data(), stats()?.models ?? [])}
title={`${data().name} Model Comparisons`}
@@ -182,7 +185,7 @@ function LabLoading() {
)
}
function LabNotFound(props: { lab: string; labs: ModelCatalogLab[] }) {
function LabNotFound(props: { lab: string; labs: RelatedCatalogLab[] }) {
const i18n = useI18n()
const labName = () => formatCatalogLabName(props.lab)
return (
@@ -194,7 +197,7 @@ function LabNotFound(props: { lab: string; labs: ModelCatalogLab[] }) {
)
}
function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
function LabHero(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[] }) {
return (
<section id="overview" data-section="lab-hero">
<LabHeroBreadcrumb label={props.lab.name} labs={props.labs} />
@@ -203,7 +206,7 @@ function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
)
}
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
function LabHeroBreadcrumb(props: { label: string; labs?: RelatedCatalogLab[] }) {
const language = useLanguage()
const labs = () => props.labs ?? []
const current = () => labs().find((lab) => lab.name === props.label)
@@ -668,7 +671,7 @@ function LabModelTooltip(props: { state: LabModelTooltipState }) {
)
}
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[]; market: MarketDay[] }) {
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[]; market: MarketDay[] }) {
const related = createMemo(() => relatedLabs(props.lab, props.labs, props.market))
return (
<section id="related-labs" data-section="model-panel" data-variant="lab-related">
@@ -757,9 +760,9 @@ function labComparisonPairs(lab: ModelCatalogLab, usage: LabUsageModelEntry[]) {
)
}
type RelatedLabEntry = { lab: ModelCatalogLab; share: number; tokens: number }
type RelatedLabEntry = { lab: RelatedCatalogLab; share: number; tokens: number }
function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
function relatedLabs(current: ModelCatalogLab, labs: RelatedCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
const stats = relatedLabStats(labs, market)
return labs
.filter((lab) => lab.id !== current.id)
@@ -768,8 +771,8 @@ function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market:
.slice(0, 3)
}
function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) {
const labByKey = new Map<string, ModelCatalogLab>()
function relatedLabStats(labs: RelatedCatalogLab[], market: MarketDay[]) {
const labByKey = new Map<string, RelatedCatalogLab>()
labs.forEach((lab) => {
labByKey.set(lab.id, lab)
labByKey.set(catalogSlug(lab.name), lab)
@@ -794,7 +797,7 @@ function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) {
)
}
function labRelatedDescription(lab: ModelCatalogLab) {
function labRelatedDescription(lab: RelatedCatalogLab) {
return lab.description ?? ""
}
@@ -17,7 +17,7 @@ export type ComparisonPair = {
description?: string
}
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
export function modelRefFromCatalog(entry: Pick<ModelCatalogEntry, "name" | "lab" | "slug">): ComparisonModelRef {
return {
name: entry.name,
lab: entry.lab,
+120
View File
@@ -0,0 +1,120 @@
import { geoEquirectangular, geoPath } from "d3-geo"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-110m.json?raw"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
export const geoMapWidth = 960
export const geoMapHeight = 430
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
export const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
}))
export const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
// The 110m topology omits small regions. Geographic centroids keep those countries interactive without shipping 50m paths.
const fallbackCountryMarkerCoordinates = [
["016", -170.7179, -14.3046],
["020", 1.5606, 42.542],
["028", -61.7945, 17.2762],
["048", 50.5425, 26.0417],
["052", -59.5602, 13.1811],
["060", -64.7558, 32.3131],
["086", 72.4453, -7.3312],
["092", -64.4704, 18.5276],
["132", -23.9576, 15.9551],
["136", -80.9129, 19.43],
["174", 43.6844, -11.879],
["184", -159.7871, -21.2195],
["212", -61.3576, 15.4394],
["234", -6.8808, 62.0527],
["239", -36.4863, -54.4641],
["248", 19.9528, 60.2153],
["258", -144.8045, -14.7283],
["296", -167.9217, 0.893],
["308", -61.6818, 12.1174],
["316", 144.767, 13.4406],
["334", 73.52, -53.0872],
["336", 12.4343, 41.9021],
["344", 114.1143, 22.3983],
["438", 9.5357, 47.1367],
["446", 113.509, 22.2231],
["462", 73.4573, 3.7316],
["470", 14.405, 35.9215],
["480", 57.5714, -20.2779],
["492", 7.4073, 43.7526],
["500", -62.1856, 16.7404],
["520", 166.9326, -0.5189],
["531", -68.9721, 12.1957],
["533", -69.9827, 12.521],
["534", -63.0572, 18.0509],
["570", -169.8704, -19.0489],
["574", 167.9497, -29.0516],
["580", 145.6193, 15.8288],
["583", 153.2966, 7.5361],
["584", 170.3313, 7.015],
["585", 134.4056, 7.286],
["612", -128.3167, -24.3649],
["652", -62.841, 17.8988],
["654", -9.7009, -12.3548],
["659", -62.6873, 17.2647],
["660", -63.066, 18.2243],
["662", -60.9696, 13.8946],
["663", -63.0599, 18.0888],
["666", -56.3037, 46.9187],
["670", -61.2008, 13.2251],
["674", 12.4594, 43.9415],
["678", 6.7235, 0.4434],
["690", 55.476, -4.6601],
["702", 103.817, 1.359],
["776", -174.7998, -20.4161],
["796", -71.9734, 21.8312],
["831", -2.5726, 49.4678],
["832", -2.1272, 49.2181],
["833", -4.5388, 54.224],
["850", -64.8028, 17.9555],
["876", -177.3469, -13.8898],
["882", -172.1649, -13.7536],
] as const
export const worldCountryMarkers = [
...worldCountries.features.flatMap((country) => {
const marker = geoCountryMarker(country)
return marker ? [{ id: String(country.id ?? "").padStart(3, "0"), marker }] : []
}),
...fallbackCountryMarkerCoordinates.flatMap(([id, longitude, latitude]) => {
const marker = worldProjection([longitude, latitude])
return marker ? [{ id, marker: { x: marker[0], y: marker[1] } }] : []
}),
]
+75 -375
View File
@@ -1,10 +1,7 @@
import { Link, Meta, Title } from "@solidjs/meta"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { geoEquirectangular, geoPath } from "d3-geo"
import { scaleSqrt } from "d3-scale"
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Regular-Latin1.woff2?url"
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url"
@@ -15,7 +12,6 @@ import {
type CountryEntry,
type LeaderboardEntry,
type MarketDay,
type StatsHomeData,
type SessionCostEntry,
type TokenCostEntry,
type UsagePoint,
@@ -23,14 +19,13 @@ import {
import { createAsync, query } from "@solidjs/router"
import { createEffect, createMemo, createSignal, For, onCleanup, 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 { runStatsEffect } from "../stats-runtime"
import { LocaleLinks } from "../component/locale-links"
import { useI18n } from "../context/i18n"
import { useLanguage } from "../context/language"
import { localizedUrl } from "../lib/language"
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
import { findModelCatalogEntry, loadModelCatalog, type ModelCatalog } from "./model-catalog"
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "./geo-map"
import { SectionHeading } from "./section-heading"
import { setStatsPageCacheHeaders } from "./stats-cache"
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
@@ -45,9 +40,6 @@ import {
type ThemePreference,
} from "./stats-shell"
const products = ["All Users", "Zen", "Go"] as const
const tokenProducts = ["Zen", "Go"] as const
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
const comparisonPairIndexes = [
[0, 1, "Top two by recent usage"],
[0, 2, "Leader vs challenger"],
@@ -69,60 +61,40 @@ const usageColors = [
"#ff6467",
]
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
const geoMapWidth = 960
const geoMapHeight = 430
type UsageProduct = (typeof products)[number]
type TokenProduct = (typeof tokenProducts)[number]
type UsageRange = (typeof ranges)[number]
type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M"
type IsoCountryCode = readonly [string, string, string]
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
function productLabel(product: UsageProduct | TokenProduct, i18n: ReturnType<typeof useI18n>) {
if (product === "All Users") return i18n.t("product.allUsers")
if (product === "Zen") return i18n.t("product.zen")
return i18n.t("product.go")
}
function rangeLabel(range: UsageRange, i18n: ReturnType<typeof useI18n>) {
if (range === "1D") return i18n.t("range.1D")
if (range === "1W") return i18n.t("range.1W")
if (range === "2W") return i18n.t("range.2W")
if (range === "1M") return i18n.t("range.1M")
return i18n.t("range.2M")
type StatsHomePageData = {
updatedAt: string | null
usage: UsagePoint[]
users: UsagePoint[]
leaderboard: LeaderboardEntry[]
market: MarketDay[]
tokenCost: TokenCostEntry[]
cacheRatio: CacheRatioEntry[]
sessionCost: SessionCostEntry[]
country: CountryEntry[]
}
const countryNumericIds = new Map(
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
)
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
marker: geoCountryMarker(country),
}))
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
const getData = query(async () => {
"use server"
return runStatsEffect(getStatsHomeData())
const [stats, catalog] = await Promise.all([runStatsEffect(getStatsHomeData()), loadModelCatalog()])
return {
updatedAt: stats.updatedAt,
usage: stats.usage.Go["2M"],
users: stats.users.Go["2M"],
leaderboard: stats.leaderboard.Go["2M"],
market: stats.market["2M"],
tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog),
cacheRatio: stats.cacheRatio.Go,
sessionCost: stats.sessionCost.Go,
country: stats.country["2M"],
} satisfies StatsHomePageData
}, "getStatsHomeData")
export default function StatsHome() {
@@ -133,7 +105,6 @@ export default function StatsHome() {
const statsHomeUrl = localizedUrl(language.locale(), "/data/")
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
const data = createAsync(() => getData())
const catalog = createAsync(() => getModelCatalog())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const updateThemePreference = (preference: ThemePreference) => {
@@ -185,12 +156,12 @@ export default function StatsHome() {
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
<UniqueUsersSection data={stats().users} />
<SessionCostSection data={stats().sessionCost} />
<TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
<TokenCostSection data={stats().tokenCost} />
<CacheRatioSection data={stats().cacheRatio} />
<MarketShareSection data={stats().market} />
<GeoBreakdownSection data={stats().country} />
<ComparisonCardsSection
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
pairs={homeComparisonPairs(stats().leaderboard)}
title="Model Comparisons"
description="Popular model pairs from the leaderboard."
variant="featured"
@@ -401,32 +372,9 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
return `${value.date}, ${value.time}`
}
function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: StatsHomeData["leaderboard"] }) {
function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardEntry[] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<UsageProduct>("Go")
const [range, setRange] = createSignal<UsageRange>("2M")
const [sheet, setSheet] = createSignal<"product" | "range">()
const [activeModel, setActiveModel] = createSignal<string>()
const data = createMemo(() => props.data[product()][range()])
const leaderboard = createMemo(() => props.leaderboard[product()][range()])
createEffect(() => {
if (!sheet()) return
if (typeof document === "undefined") return
const htmlOverflow = document.documentElement.style.overflow
const bodyOverflow = document.body.style.overflow
document.documentElement.style.overflow = "hidden"
document.body.style.overflow = "hidden"
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setSheet(undefined)
}
document.addEventListener("keydown", onKeyDown)
onCleanup(() => {
document.documentElement.style.overflow = htmlOverflow
document.body.style.overflow = bodyOverflow
document.removeEventListener("keydown", onKeyDown)
})
})
return (
<section id="top-models" data-section="top-models">
@@ -438,201 +386,23 @@ function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: St
description={i18n.t("home.topModelsDescription")}
/>
<Show
when={data().some((item) => usageTotal(item) > 0)}
when={props.data.some((item) => usageTotal(item) > 0)}
fallback={<EmptyState title={i18n.t("home.noUsageTitle")} description={i18n.t("home.noUsageDescription")} />}
>
<TopModelsChart
data={data()}
range={range()}
activeModel={activeModel()}
onActiveModelChange={setActiveModel}
/>
<TopModelsChart data={props.data} range="2M" activeModel={activeModel()} onActiveModelChange={setActiveModel} />
</Show>
<Show
when={leaderboard().length > 0}
when={props.leaderboard.length > 0}
fallback={
<EmptyState title={i18n.t("home.noLeaderboardTitle")} description={i18n.t("home.noLeaderboardDescription")} />
}
>
<Leaderboard data={leaderboard()} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
</Show>
<div data-slot="chart-footer" hidden>
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
<div data-slot="top-models-mobile-controls">
<MobileFilterButton
label={i18n.t("home.productFilter")}
value={productLabel(product(), i18n)}
expanded={sheet() === "product"}
onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
/>
<MobileFilterButton
label={i18n.t("home.dateRange")}
value={rangeLabel(range(), i18n)}
expanded={sheet() === "range"}
onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
/>
</div>
</div>
<Show when={sheet()}>
{(kind) => (
<MobileFilterSheet
kind={kind()}
product={product()}
range={range()}
onProductSelect={(value) => {
setProduct(value)
setSheet(undefined)
}}
onRangeSelect={(value) => {
setRange(value)
setSheet(undefined)
}}
onClose={() => setSheet(undefined)}
/>
)}
<Leaderboard data={props.leaderboard} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
</Show>
</section>
)
}
function MobileFilterButton(props: { label: string; value: string; expanded: boolean; onClick: () => void }) {
return (
<button
data-slot="mobile-filter-button"
type="button"
aria-label={props.label}
aria-expanded={props.expanded ? "true" : "false"}
onClick={props.onClick}
>
<span>{props.value}</span>
<ChevronDown />
</button>
)
}
function MobileFilterSheet(props: {
kind: "product" | "range"
product: UsageProduct
range: UsageRange
onProductSelect: (product: UsageProduct) => void
onRangeSelect: (range: UsageRange) => void
onClose: () => void
}) {
const i18n = useI18n()
return (
<div data-component="mobile-filter-sheet" role="presentation" onClick={props.onClose}>
<div
data-slot="filter-sheet-panel"
role="radiogroup"
aria-label={props.kind === "product" ? i18n.t("home.productFilter") : i18n.t("home.dateRange")}
>
<Show
when={props.kind === "product"}
fallback={
<For each={ranges}>
{(item) => (
<button
type="button"
role="radio"
aria-checked={props.range === item}
data-active={props.range === item ? "true" : undefined}
onClick={(event) => {
event.stopPropagation()
props.onRangeSelect(item)
}}
>
{rangeLabel(item, i18n)}
</button>
)}
</For>
}
>
<For each={products}>
{(item) => (
<button
type="button"
role="radio"
aria-checked={props.product === item}
data-active={props.product === item ? "true" : undefined}
onClick={(event) => {
event.stopPropagation()
props.onProductSelect(item)
}}
>
{productLabel(item, i18n)}
</button>
)}
</For>
</Show>
</div>
</div>
)
}
function ChevronDown() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
<path d="M5 7L8 10L11 7" stroke="currentColor" />
</svg>
)
}
function StatsFilters(props: {
product: UsageProduct
range: UsageRange
onProductSelect: (product: UsageProduct) => void
onRangeSelect: (range: UsageRange) => void
}) {
const i18n = useI18n()
return (
<>
<FilterPills
items={products}
selected={props.product}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={props.onProductSelect}
/>
<FilterPills
items={ranges}
selected={props.range}
label={i18n.t("home.dateRange")}
variant="range"
formatLabel={(item) => rangeLabel(item, i18n)}
onSelect={props.onRangeSelect}
/>
</>
)
}
function FilterPills<T extends string>(props: {
items: readonly T[]
selected: T
label: string
variant: "product" | "range"
formatLabel?: (item: T) => string
onSelect: (item: T) => void
}) {
return (
<div data-component="usage-filter" data-variant={props.variant} role="radiogroup" aria-label={props.label}>
<For each={props.items}>
{(item) => (
<button
type="button"
role="radio"
aria-checked={props.selected === item}
data-active={props.selected === item ? "true" : undefined}
onClick={() => props.onSelect(item)}
>
{props.formatLabel ? props.formatLabel(item) : item}
</button>
)}
</For>
</div>
)
}
function TopModelsChart(props: {
data: UsagePoint[]
range: UsageRange
@@ -823,10 +593,9 @@ function TopModelsChart(props: {
)
}
function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
function UniqueUsersSection(props: { data: UsagePoint[] }) {
const i18n = useI18n()
const [activeModel, setActiveModel] = createSignal<string>()
const data = createMemo(() => props.data.Go["2M"])
return (
<section id="unique-users" data-section="unique-users">
@@ -837,13 +606,13 @@ function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
description={i18n.t("home.uniqueUsersDescription")}
/>
<Show
when={data().some((item) => usageTotal(item) > 0)}
when={props.data.some((item) => usageTotal(item) > 0)}
fallback={
<EmptyState title={i18n.t("home.noUserDataTitle")} description={i18n.t("home.noUserDataDescription")} />
}
>
<TopModelsChart
data={data()}
data={props.data}
range="2M"
metric="users"
ariaLabel={i18n.t("home.uniqueUsersChart")}
@@ -1084,16 +853,14 @@ function formatChange(value: number | null, i18n: ReturnType<typeof useI18n>) {
return `${value}%`
}
function MarketShareSection(props: { data: StatsHomeData["market"] }) {
function MarketShareSection(props: { data: MarketDay[] }) {
const i18n = useI18n()
const [range, setRange] = createSignal<UsageRange>("2M")
const [activeIndex, setActiveIndex] = createSignal(2)
const [activeAuthor, setActiveAuthor] = createSignal<string>()
const [inspecting, setInspecting] = createSignal(false)
const data = createMemo(() => props.data[range()])
const authorOrder = createMemo(() => getMarketAuthorOrder(data()))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
const activeDay = createMemo(() => data()[selectedIndex()])
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
const activeDay = createMemo(() => props.data[selectedIndex()])
return (
<section
@@ -1118,8 +885,8 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
{(day) => (
<>
<MarketShare
data={data()}
range={range()}
data={props.data}
range="2M"
authorOrder={authorOrder()}
activeIndex={selectedIndex()}
activeAuthor={activeAuthor()}
@@ -1151,22 +918,9 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
<strong>
{inspecting()
? formatMarketDate(activeDay(), i18n.t("home.noData"))
: formatMarketRange(data(), i18n.t("home.noData"))}
: formatMarketRange(props.data, i18n.t("home.noData"))}
</strong>
</p>
<div hidden>
<FilterPills
items={ranges}
selected={range()}
label={i18n.t("home.dateRange")}
variant="range"
onSelect={(item) => {
setRange(item)
setActiveAuthor(undefined)
setInspecting(false)
}}
/>
</div>
</div>
</section>
)
@@ -1333,23 +1087,22 @@ function MarketShareList(props: {
)
}
function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
function GeoBreakdownSection(props: { data: CountryEntry[] }) {
const i18n = useI18n()
const language = useLanguage()
const [activeCountry, setActiveCountry] = createSignal<string>()
const data = createMemo(() => props.data["2M"])
const countryById = createMemo(
() =>
new Map(
data().flatMap((country) => {
props.data.flatMap((country) => {
const id = countryNumericId(country.country)
return id ? [[id, country] as const] : []
}),
),
)
const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1)
const topCountries = createMemo(() => data().slice(0, 15))
const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0])
const maxTokens = createMemo(() => Math.max(0, ...props.data.map((country) => country.tokens)) || 1)
const topCountries = createMemo(() => props.data.slice(0, 15))
const active = createMemo(() => props.data.find((country) => country.country === activeCountry()) ?? props.data[0])
return (
<section
@@ -1363,7 +1116,7 @@ function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
<SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
<Show
when={data().length > 0}
when={props.data.length > 0}
fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
>
<div data-component="geo-breakdown">
@@ -1453,31 +1206,30 @@ function GeoWorldMap(props: {
</For>
</g>
<g data-slot="geo-country-markers">
<For each={worldCountryPaths}>
<For each={worldCountryMarkers}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<Show when={country.marker && entry() ? country.marker : undefined}>
{(marker) => (
<circle
cx={marker().x}
cy={marker().y}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
)}
<Show when={entry()}>
<circle
cx={country.marker.x}
cy={country.marker.y}
data-country-id={country.id}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
</Show>
)
}}
@@ -1531,14 +1283,6 @@ function countryNumericId(country: string) {
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
}
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
function formatCountryName(country: string, locale: string, unknown: string) {
const code = country.toUpperCase()
if (code === "ZZ") return unknown
@@ -1636,12 +1380,10 @@ function marketDateParts(label: string) {
return { start: start ?? label, end: end ?? start ?? label }
}
function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: ModelCatalog | null }) {
function TokenCostSection(props: { data: TokenCostEntry[] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<TokenProduct>("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
const data = createMemo(() => priceTokenCostFromCatalog(props.data[product()], props.catalog))
const visible = createMemo(() => data().slice(0, 13))
const visible = createMemo(() => props.data.slice(0, 13))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
return (
@@ -1660,17 +1402,6 @@ function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: Mo
>
<TokenCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
</Show>
<div data-slot="token-footer" hidden>
<FilterPills
items={tokenProducts}
selected={product()}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={setProduct}
/>
<LiveIndicator />
</div>
</section>
)
}
@@ -1723,12 +1454,10 @@ function TokenCostChart(props: {
)
}
function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<TokenProduct>("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
const data = createMemo(() => props.data[product()])
const visible = createMemo(() => data().slice(0, 16))
const visible = createMemo(() => props.data.slice(0, 16))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
return (
@@ -1745,17 +1474,6 @@ function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
>
<CacheRatioChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
</Show>
<div data-slot="token-footer" hidden>
<FilterPills
items={tokenProducts}
selected={product()}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={setProduct}
/>
<LiveIndicator />
</div>
</section>
)
}
@@ -1853,12 +1571,10 @@ function MetricBar(props: { value: number; max: number; active: boolean }) {
)
}
function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
function SessionCostSection(props: { data: SessionCostEntry[] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<TokenProduct>("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
const data = createMemo(() => props.data[product()])
const visible = createMemo(() => data().slice(0, 16))
const visible = createMemo(() => props.data.slice(0, 16))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
return (
@@ -1877,17 +1593,6 @@ function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
>
<SessionCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
</Show>
<div data-slot="token-footer" hidden>
<FilterPills
items={tokenProducts}
selected={product()}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={setProduct}
/>
<LiveIndicator />
</div>
</section>
)
}
@@ -1949,11 +1654,6 @@ function SessionCostChart(props: {
)
}
function LiveIndicator() {
const i18n = useI18n()
return <span data-component="live-filter">{i18n.t("chart.live")}</span>
}
function formatTokenCount(value: number) {
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
return `${Math.round(value / 1_000)}K`
@@ -0,0 +1,39 @@
diff --git a/dist/index.js b/dist/index.js
index dca128d3a790378c51a24a16d92585178343b278..da75f9d64acd2b607abd15079ce2d03b7a8d675a 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -696,7 +696,7 @@ var OpenAICompatibleChatLanguageModel = class {
finishReason = { unified: "error", raw: void 0 };
controller.enqueue({
type: "error",
- error: chunk.value.error.message
+ error: chunk.value.error
});
return;
}
diff --git a/dist/index.mjs b/dist/index.mjs
index 3b1e1b6bdec5032e3b4fa5ffbcc8cdf3dfe1cc40..eaffc446f80552ea0b26573b89daa4dfc7776e6e 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -683,7 +683,7 @@ var OpenAICompatibleChatLanguageModel = class {
finishReason = { unified: "error", raw: void 0 };
controller.enqueue({
type: "error",
- error: chunk.value.error.message
+ error: chunk.value.error
});
return;
}
diff --git a/src/chat/openai-compatible-chat-language-model.ts b/src/chat/openai-compatible-chat-language-model.ts
index 8c622db23c2d9a7373701f5a1b0c2ba109e24602..643c3db68a6043e097edc1122e0eb53fd13495c5 100644
--- a/src/chat/openai-compatible-chat-language-model.ts
+++ b/src/chat/openai-compatible-chat-language-model.ts
@@ -442,7 +442,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
finishReason = { unified: 'error', raw: undefined };
controller.enqueue({
type: 'error',
- error: chunk.value.error.message,
+ error: chunk.value.error,
});
return;
}