cloudaxe-opencode/packages/stats/core/src/domain/model.ts
opencode-agent[bot] 43ecf3ff1b
chore: merge dev into v2 (#36312)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: James Long <jlongster@users.noreply.github.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
2026-07-10 15:11:29 -05:00

262 lines
9.6 KiB
TypeScript

import { and, asc, eq, inArray, max, or } from "drizzle-orm"
import { Effect, Layer } from "effect"
import * as Context from "effect/Context"
import { DatabaseError, DrizzleClient } from "../database"
import { modelStat } from "../database/schema"
import { RETIRED_STAT_MODELS, RETIRED_STAT_PROVIDERS } from "./model-normalization"
import {
chunks,
collapseRows,
inserted,
isMissingUniqueUsersColumn,
omitUniqueUsers,
rankBy,
statPeriodKey,
statRowScope,
synthesizeAllTierRows,
toStatBaseRow,
UPSERT_CHUNK_SIZE,
type StatBaseAggregate,
} from "./stat"
export type ModelStatRow = typeof modelStat.$inferInsert
export type ModelStatAggregate = StatBaseAggregate & { provider: string; model: string; provider_model: string }
export type ModelStatMetric = {
periodKey: string
updatedAt: Date
tier: string
provider: string
model: string
sessions: number
uniqueUsers: number
inputTokens: number
outputTokens: number
reasoningTokens: number
cacheReadTokens: number
totalTokens: number
inputCostMicrocents: number
outputCostMicrocents: number
totalCostMicrocents: number
}
export declare namespace ModelStatRepo {
export interface Service {
readonly listDaily: () => Effect.Effect<ModelStatMetric[], DatabaseError>
readonly lastSyncedAt: () => Effect.Effect<Date | null, DatabaseError>
readonly upsert: (rows: ModelStatRow[]) => Effect.Effect<void, DatabaseError>
readonly deleteRetiredDimensions: (rows: ModelStatRow[]) => Effect.Effect<void, DatabaseError>
}
}
export class ModelStatRepo extends Context.Service<ModelStatRepo, ModelStatRepo.Service>()(
"@opencode/stats/ModelStatRepo",
) {
static readonly layer: Layer.Layer<ModelStatRepo, never, DrizzleClient> = Layer.effect(
ModelStatRepo,
Effect.gen(function* () {
const db = yield* DrizzleClient
const listDaily = Effect.fn("ModelStatRepo.listDaily")(function* () {
return yield* Effect.tryPromise({
try: async () => {
try {
return await db
.select({
periodKey: modelStat.period_key,
updatedAt: modelStat.updated_at,
tier: modelStat.tier,
provider: modelStat.provider,
model: modelStat.model,
sessions: modelStat.sessions,
uniqueUsers: modelStat.unique_users,
inputTokens: modelStat.input_tokens,
outputTokens: modelStat.output_tokens,
reasoningTokens: modelStat.reasoning_tokens,
cacheReadTokens: modelStat.cache_read_tokens,
totalTokens: modelStat.total_tokens,
inputCostMicrocents: modelStat.input_cost_microcents,
outputCostMicrocents: modelStat.output_cost_microcents,
totalCostMicrocents: modelStat.total_cost_microcents,
})
.from(modelStat)
.where(modelDailyScope())
.orderBy(asc(modelStat.period_key))
} catch (cause) {
if (!isMissingUniqueUsersColumn(cause)) throw cause
return (
await db
.select({
periodKey: modelStat.period_key,
updatedAt: modelStat.updated_at,
tier: modelStat.tier,
provider: modelStat.provider,
model: modelStat.model,
sessions: modelStat.sessions,
inputTokens: modelStat.input_tokens,
outputTokens: modelStat.output_tokens,
reasoningTokens: modelStat.reasoning_tokens,
cacheReadTokens: modelStat.cache_read_tokens,
totalTokens: modelStat.total_tokens,
inputCostMicrocents: modelStat.input_cost_microcents,
outputCostMicrocents: modelStat.output_cost_microcents,
totalCostMicrocents: modelStat.total_cost_microcents,
})
.from(modelStat)
.where(modelDailyScope())
.orderBy(asc(modelStat.period_key))
).map((row) => ({ ...row, uniqueUsers: 0 }))
}
},
catch: (cause) => DatabaseError.make({ cause }),
})
})
const lastSyncedAt = Effect.fn("ModelStatRepo.lastSyncedAt")(function* () {
const result = yield* Effect.tryPromise({
try: () => db.select({ value: max(modelStat.updated_at) }).from(modelStat),
catch: (cause) => DatabaseError.make({ cause }),
})
return result[0]?.value ?? null
})
const upsert = Effect.fn("ModelStatRepo.upsert")(function* (rows: ModelStatRow[]) {
yield* Effect.forEach(
chunks(rows, UPSERT_CHUNK_SIZE),
(chunk) =>
Effect.tryPromise({
try: async () => {
try {
return await upsertModelChunk(chunk, true)
} catch (cause) {
if (!isMissingUniqueUsersColumn(cause)) throw cause
return upsertModelChunk(chunk, false)
}
},
catch: (cause) => DatabaseError.make({ cause }),
}),
{ discard: true },
)
})
function upsertModelChunk(chunk: ModelStatRow[], includeUniqueUsers: boolean) {
return db
.insert(modelStat)
.values(includeUniqueUsers ? chunk : omitUniqueUsers(chunk))
.onDuplicateKeyUpdate({
set: {
provider_model: inserted("provider_model"),
sessions: inserted("sessions"),
requests: inserted("requests"),
...(includeUniqueUsers ? { unique_users: inserted("unique_users") } : {}),
input_tokens: inserted("input_tokens"),
output_tokens: inserted("output_tokens"),
reasoning_tokens: inserted("reasoning_tokens"),
cache_read_tokens: inserted("cache_read_tokens"),
total_tokens: inserted("total_tokens"),
input_cost_microcents: inserted("input_cost_microcents"),
output_cost_microcents: inserted("output_cost_microcents"),
total_cost_microcents: inserted("total_cost_microcents"),
avg_duration_ms: inserted("avg_duration_ms"),
p50_duration_ms: inserted("p50_duration_ms"),
p95_duration_ms: inserted("p95_duration_ms"),
avg_ttfb_ms: inserted("avg_ttfb_ms"),
p50_ttfb_ms: inserted("p50_ttfb_ms"),
p95_ttfb_ms: inserted("p95_ttfb_ms"),
avg_output_tps: inserted("avg_output_tps"),
success_count: inserted("success_count"),
error_count: inserted("error_count"),
sample_count: inserted("sample_count"),
rank_by_tokens: inserted("rank_by_tokens"),
rank_by_requests: inserted("rank_by_requests"),
rank_by_cost: inserted("rank_by_cost"),
},
})
}
const deleteRetiredDimensions = Effect.fn("ModelStatRepo.deleteRetiredDimensions")(function* (
rows: ModelStatRow[],
) {
const scope = statRowScope(rows)
if (!scope) return
yield* Effect.tryPromise({
try: () =>
db
.delete(modelStat)
.where(
and(
inArray(modelStat.grain, scope.grains),
inArray(modelStat.period_key, scope.periodKeys),
inArray(modelStat.dataset, scope.datasets),
inArray(modelStat.client, scope.clients),
inArray(modelStat.source, scope.sources),
or(
inArray(modelStat.provider, RETIRED_STAT_PROVIDERS),
inArray(modelStat.model, RETIRED_STAT_MODELS),
),
),
),
catch: (cause) => DatabaseError.make({ cause }),
})
})
return ModelStatRepo.of({ listDaily, lastSyncedAt, upsert, deleteRetiredDimensions })
}),
)
}
function modelDailyScope() {
return and(
eq(modelStat.grain, "day"),
eq(modelStat.client, "all"),
eq(modelStat.source, "all"),
inArray(modelStat.tier, ["Go", "go"]),
)
}
export function rowsFromAggregates(aggregates: ModelStatAggregate[]) {
return rankRows([
...synthesizeAllTierRows(
collapseRows(aggregates.filter((item) => item.grain === "week").map(toRow), dimensionKey),
dimensionKey,
),
...synthesizeAllTierRows(
collapseRows(aggregates.filter((item) => item.grain === "day").map(toRow), dimensionKey),
dimensionKey,
),
])
}
function toRow(data: ModelStatAggregate): ModelStatRow {
return {
...toStatBaseRow(data),
provider: data.provider,
model: data.model,
provider_model: data.provider_model,
}
}
function rankRows(rows: ModelStatRow[]) {
return Object.values(
rows.reduce<Record<string, ModelStatRow[]>>((result, row) => {
const key = statPeriodKey(row)
result[key] = [...(result[key] ?? []), row]
return result
}, {}),
).flatMap((group) => {
const tokenRanks = rankBy(group, (row) => row.total_tokens ?? 0)
const requestRanks = rankBy(group, (row) => row.requests ?? 0)
const costRanks = rankBy(group, (row) => row.total_cost_microcents ?? 0)
return group.map((row) => ({
...row,
rank_by_tokens: tokenRanks.get(row) ?? null,
rank_by_requests: requestRanks.get(row) ?? null,
rank_by_cost: costRanks.get(row) ?? null,
}))
})
}
function dimensionKey(row: ModelStatRow) {
return [row.provider, row.model].join("\u0000")
}