Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/dashboard/migrations/0003_model_pricing_cache.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE model_pricing_cache (
match_key TEXT PRIMARY KEY NOT NULL,
requested_model TEXT NOT NULL,
source_model TEXT,
source_provider TEXT,
input_cost_per_token REAL,
output_cost_per_token REAL,
cache_read_input_cost_per_token REAL,
resolved INTEGER NOT NULL DEFAULT 0,
fetched_at INTEGER NOT NULL,
source_url TEXT NOT NULL
);

CREATE INDEX model_pricing_cache_fetched_at_idx
ON model_pricing_cache (fetched_at);
17 changes: 17 additions & 0 deletions apps/dashboard/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,20 @@ export const issueEvents = sqliteTable(
index('issue_events_severity_idx').on(table.severity),
],
)

export const modelPricingCache = sqliteTable(
'model_pricing_cache',
{
matchKey: text('match_key').primaryKey(),
requestedModel: text('requested_model').notNull(),
sourceModel: text('source_model'),
sourceProvider: text('source_provider'),
inputCostPerToken: real('input_cost_per_token'),
outputCostPerToken: real('output_cost_per_token'),
cacheReadInputCostPerToken: real('cache_read_input_cost_per_token'),
resolved: integer('resolved').notNull().default(0),
fetchedAt: integer('fetched_at').notNull(),
sourceUrl: text('source_url').notNull(),
},
(table) => [index('model_pricing_cache_fetched_at_idx').on(table.fetchedAt)],
)
3 changes: 3 additions & 0 deletions apps/dashboard/src/lib/dashboard-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function filterSnapshotByProjects(snapshot: DashboardSnapshot, selectedPr
rangeLabel: snapshot.headline.rangeLabel,
selectedProjectIds: filteredProjectIds,
sourceLabel: snapshot.headline.sourceLabel,
pricingStatus: snapshot.headline.pricing,
statusNote: snapshot.headline.summary,
workspaceName: summarizeProjectSelection(snapshot.projects.available, filteredProjectIds),
})
Expand Down Expand Up @@ -75,6 +76,7 @@ function summarizeModels(modelRows: DashboardModelDailyUsage[]): DashboardModelS
const current = modelMap.get(key)
if (current) {
current.cost += row.cost
current.projectedCost = (current.projectedCost || 0) + (row.projectedCost || 0)
current.requests += row.requests
current.tokens += row.tokens
continue
Expand All @@ -83,6 +85,7 @@ function summarizeModels(modelRows: DashboardModelDailyUsage[]): DashboardModelS
modelMap.set(key, {
cost: row.cost,
model: row.model,
projectedCost: row.projectedCost || 0,
provider: row.provider,
requests: row.requests,
tokens: row.tokens,
Expand Down
4 changes: 4 additions & 0 deletions apps/dashboard/src/lib/dashboard-timeframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function filterSnapshotByTimeframe(snapshot: DashboardSnapshot, selection
rangeLabel: resolved.rangeLabel,
selectedProjectIds: snapshot.filters.selectedProjectIds,
sourceLabel: snapshot.headline.sourceLabel,
pricingStatus: snapshot.headline.pricing,
statusNote: snapshot.headline.summary,
workspaceName: snapshot.headline.workspace,
})
Expand Down Expand Up @@ -81,6 +82,7 @@ export function filterSnapshotByTimeframe(snapshot: DashboardSnapshot, selection
rangeLabel: resolved.rangeLabel,
selectedProjectIds: snapshot.filters.selectedProjectIds,
sourceLabel: snapshot.headline.sourceLabel,
pricingStatus: snapshot.headline.pricing,
statusNote: snapshot.headline.summary,
workspaceName: snapshot.headline.workspace,
})
Expand Down Expand Up @@ -125,6 +127,7 @@ function summarizeModels(modelRows: DashboardModelDailyUsage[]): DashboardModelS
const current = modelMap.get(key)
if (current) {
current.cost += row.cost
current.projectedCost = (current.projectedCost || 0) + (row.projectedCost || 0)
current.requests += row.requests
current.tokens += row.tokens
continue
Expand All @@ -133,6 +136,7 @@ function summarizeModels(modelRows: DashboardModelDailyUsage[]): DashboardModelS
modelMap.set(key, {
cost: row.cost,
model: row.model,
projectedCost: row.projectedCost || 0,
provider: row.provider,
requests: row.requests,
tokens: row.tokens,
Expand Down
84 changes: 78 additions & 6 deletions apps/dashboard/src/lib/openai-usage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { formatHoustonDay, formatHoustonTimestamp } from '#/lib/dashboard-timezone'
import {
ensureModelPricingForReferences,
estimateProjectedCostUsd,
getModelPricingLookupKey,
} from '#/lib/public-model-pricing'
import type { ModelPricingLookupRow } from '#/lib/public-model-pricing'
import type { CloudflareAppEnv } from '#/lib/runtime'
import type {
DashboardIssueByDay,
Expand Down Expand Up @@ -27,6 +33,7 @@ type DailyRollupRow = DashboardProjectOption & {
type ModelSummaryRow = {
cost: number
model: string
projectedCost?: number
provider: string
requests: number
tokens: number
Expand Down Expand Up @@ -527,23 +534,40 @@ async function loadSnapshotFromD1(
dailyModelRowsByDay.length > 0
? dailyModelRowsByDay
: aggregateModelRowsByDay(hourlyModelRowsByDay)
const models =
const pricingResult = await ensureModelPricingForReferences(
env,
resolvedModelRowsByDay.map((row) => ({
model: row.model,
provider: row.provider,
tokens: row.tokens,
})),
)
const enrichedModelRowsByDay = applyProjectedPricingToModelRows(
resolvedDailyRows,
resolvedModelRowsByDay,
pricingResult.lookup,
)
const enrichedHourlyModelRowsByDay =
hourlyModelRowsByDay.length > 0
? summarizeModelRows(resolvedModelRowsByDay)
: await loadModelSummary(env.DB, workspaceIds, firstDay, lastDay)
? applyProjectedPricingToModelRows(hourlyRows, hourlyModelRowsByDay, pricingResult.lookup)
: undefined
const models = summarizeModelRows(enrichedModelRowsByDay)

return buildSnapshotFromRollups({
availableProjects,
dailyRows: resolvedDailyRows,
environment: rows[rows.length - 1].environment,
generatedAt: new Date(latestCreatedAt).toISOString(),
hourlyModelRowsByDay:
hourlyModelRowsByDay.length > 0 ? hourlyModelRowsByDay : undefined,
enrichedHourlyModelRowsByDay && enrichedHourlyModelRowsByDay.length > 0
? enrichedHourlyModelRowsByDay
: undefined,
hourlyRows: hourlyRows.length > 0 ? hourlyRows : undefined,
issues: summarizeIssues(issuesByDay),
issuesByDay,
models,
modelRowsByDay: resolvedModelRowsByDay,
modelRowsByDay: enrichedModelRowsByDay,
pricingStatus: pricingResult.status,
selectedProjectIds: availableProjects.map((project) => project.projectId),
sourceLabel,
statusNote: buildCombinedStatusNote(selections),
Expand Down Expand Up @@ -716,7 +740,7 @@ async function loadDailyRollups(
return result.results
}

async function loadModelSummary(
export async function loadModelSummary(
db: D1Database,
workspaceIds: string[],
startDay: string,
Expand Down Expand Up @@ -871,6 +895,7 @@ function aggregateModelRowsByDay(rows: DashboardModelDailyUsage[]) {
const current = rowMap.get(key)
if (current) {
current.cost += row.cost
current.projectedCost = roundCurrency((current.projectedCost || 0) + (row.projectedCost || 0))
current.requests += row.requests
current.tokens += row.tokens
continue
Expand All @@ -885,6 +910,51 @@ function aggregateModelRowsByDay(rows: DashboardModelDailyUsage[]) {
)
}

function applyProjectedPricingToModelRows(
rollupRows: DailyRollupRow[],
modelRows: DashboardModelDailyUsage[],
pricingLookup: Map<string, ModelPricingLookupRow>,
) {
const rollupMap = new Map<string, DailyRollupRow>()
for (const row of rollupRows) {
rollupMap.set(`${row.projectId}:${row.day}`, row)
}

return modelRows.map((row) => {
const rollup = rollupMap.get(`${row.projectId}:${row.day}`)
if (!rollup || rollup.totalTokens <= 0) {
return { ...row, projectedCost: 0 }
}

const estimatedInputTokens = resolveProjectedTokenSlice(rollup.inputTokens, rollup.totalTokens, row.tokens)
const estimatedOutputTokens = resolveProjectedTokenSlice(rollup.outputTokens, rollup.totalTokens, row.tokens)
const estimatedCachedTokens = resolveProjectedTokenSlice(rollup.cachedTokens, rollup.totalTokens, row.tokens)
const pricing = pricingLookup.get(getModelPricingLookupKey(row.model))

return {
...row,
projectedCost: estimateProjectedCostUsd({
cacheReadInputTokens: estimatedCachedTokens,
inputTokens: estimatedInputTokens,
outputTokens: estimatedOutputTokens,
pricing,
}),
}
})
}

function resolveProjectedTokenSlice(
bucketTokens: number,
bucketTotalTokens: number,
modelTokens: number,
) {
if (bucketTokens <= 0 || bucketTotalTokens <= 0 || modelTokens <= 0) {
return 0
}

return Math.max(0, Math.round(modelTokens * Math.min(1, bucketTokens / bucketTotalTokens)))
}

function summarizeModelRows(rows: DashboardModelDailyUsage[]) {
const modelMap = new Map<string, ModelSummaryRow>()

Expand All @@ -893,6 +963,7 @@ function summarizeModelRows(rows: DashboardModelDailyUsage[]) {
const current = modelMap.get(key)
if (current) {
current.cost += row.cost
current.projectedCost = roundCurrency((current.projectedCost || 0) + (row.projectedCost || 0))
current.requests += row.requests
current.tokens += row.tokens
continue
Expand All @@ -901,6 +972,7 @@ function summarizeModelRows(rows: DashboardModelDailyUsage[]) {
modelMap.set(key, {
cost: row.cost,
model: row.model,
projectedCost: row.projectedCost || 0,
provider: row.provider,
requests: row.requests,
tokens: row.tokens,
Expand Down
Loading
Loading