Skip to content

Commit 5e7236e

Browse files
author
Zoo (VP)
committed
fix(dashboard): resolve custom model pricing for openai-compatible providers and day rollup
1 parent a56a584 commit 5e7236e

13 files changed

Lines changed: 535 additions & 75 deletions

‎src/api/transform/__tests__/gemini-format.spec.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe("convertAnthropicMessageToGemini", () => {
9999
source: {
100100
type: "url", // Not supported
101101
url: "https://example.com/image.jpg",
102-
} as any,
102+
} as unknown as Anthropic.Messages.ImageBlockParam["source"],
103103
},
104104
],
105105
}
@@ -149,7 +149,10 @@ describe("convertAnthropicMessageToGemini", () => {
149149
const anthropicMessage: Anthropic.Messages.MessageParam = {
150150
role: "assistant",
151151
content: [
152-
{ type: "thoughtSignature", thoughtSignature: "sig-123" } as any,
152+
{
153+
type: "thoughtSignature",
154+
thoughtSignature: "sig-123",
155+
} as unknown as Anthropic.Messages.ContentBlockParam,
153156
{ type: "tool_use", id: "call-1", name: "toolA", input: { a: 1 } },
154157
{ type: "tool_use", id: "call-2", name: "toolB", input: { b: 2 } },
155158
],
@@ -158,7 +161,7 @@ describe("convertAnthropicMessageToGemini", () => {
158161
const result = convertAnthropicMessageToGemini(anthropicMessage)
159162
expect(result).toHaveLength(1)
160163

161-
const parts = result[0]!.parts as any[]
164+
const parts = result[0]!.parts as Array<{ functionCall?: unknown; thoughtSignature?: string }>
162165
const functionCallParts = parts.filter((p) => p.functionCall)
163166
expect(functionCallParts).toHaveLength(2)
164167

@@ -240,7 +243,7 @@ describe("convertAnthropicMessageToGemini", () => {
240243
{
241244
type: "tool_result",
242245
tool_use_id: "calculator-123",
243-
content: null as any,
246+
content: null as unknown as string,
244247
},
245248
],
246249
}
@@ -616,7 +619,7 @@ describe("convertAnthropicMessageToGemini", () => {
616619
{
617620
type: "unknown_type", // Unsupported type
618621
data: "some data",
619-
} as any,
622+
} as unknown as Anthropic.Messages.ContentBlockParam,
620623
{ type: "text", text: "Valid content" },
621624
],
622625
}
@@ -636,9 +639,9 @@ describe("convertAnthropicMessageToGemini", () => {
636639
role: "assistant",
637640
content: [
638641
{
639-
type: "reasoning" as any,
642+
type: "reasoning",
640643
text: "Let me think about this...",
641-
},
644+
} as unknown as Anthropic.Messages.ContentBlockParam,
642645
{ type: "text", text: "Here's my answer" },
643646
],
644647
}

‎src/services/stats/DashboardTaskProjection.ts‎

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
getEffectiveCost,
1313
computeCacheDiscountBase,
1414
computeCacheDiscountBaseFromAggregated,
15+
computeCostFromAggregated,
1516
applyCacheDiscount,
17+
customPricingKey,
1618
} from "./costRecalculation"
1719
import type { CustomModelPricingMap } from "./costRecalculation"
1820
import { type StatsQueryRangeMs } from "./statsQueryRange"
@@ -53,7 +55,15 @@ export interface DashboardTaskUsageReader {
5355
taskIds: string[],
5456
rangeMs?: StatsQueryRangeMs,
5557
includeCancelled?: boolean,
56-
): Array<{ taskId: string; provider: string; model: string; inputTokens: number }>
58+
): Array<{
59+
taskId: string
60+
provider: string
61+
model: string
62+
inputTokens: number
63+
outputTokens?: number
64+
cacheWriteTokens?: number
65+
cacheReadTokens?: number
66+
}>
5767
}
5868

5969
/**
@@ -224,7 +234,15 @@ function computeTaskSummary(
224234
identityByTaskId: ReadonlyMap<string, TaskIdentityAggregate>,
225235
cacheRatio?: number,
226236
customPricing?: CustomModelPricingMap,
227-
inputTokensByTaskId: Array<{ taskId: string; provider: string; model: string; inputTokens: number }> = [],
237+
inputTokensByTaskId: Array<{
238+
taskId: string
239+
provider: string
240+
model: string
241+
inputTokens: number
242+
outputTokens?: number
243+
cacheWriteTokens?: number
244+
cacheReadTokens?: number
245+
}> = [],
228246
): DashboardTaskSummary {
229247
const task = catalog.byId.get(taskId)!
230248
const subtreeUsage = summarizeSubtreeUsage(
@@ -262,7 +280,15 @@ function summarizeSubtreeUsage(
262280
identityByTaskId: ReadonlyMap<string, TaskIdentityAggregate>,
263281
cacheRatio?: number,
264282
customPricing?: CustomModelPricingMap,
265-
inputTokensByTaskId: Array<{ taskId: string; provider: string; model: string; inputTokens: number }> = [],
283+
inputTokensByTaskId: Array<{
284+
taskId: string
285+
provider: string
286+
model: string
287+
inputTokens: number
288+
outputTokens?: number
289+
cacheWriteTokens?: number
290+
cacheReadTokens?: number
291+
}> = [],
266292
): SubtreeUsageSummary {
267293
let totalCost = 0
268294
let totalCacheDiscountBase = 0
@@ -274,13 +300,30 @@ function summarizeSubtreeUsage(
274300
const subtreeModels: string[] = []
275301
const subtreeModes: string[] = []
276302

277-
// Build a lookup map for per-(task, provider, model) input tokens
278-
// so we can recompute cacheDiscountBase at query time when customPricing
303+
// Build a lookup map for per-(task, provider, model) token counts
304+
// so we can recompute cost and cacheDiscountBase at query time when customPricing
279305
// is available (the stored value was 0 for custom models).
280-
const inputTokensLookup = new Map<string, Array<{ provider: string; model: string; inputTokens: number }>>()
306+
const inputTokensLookup = new Map<
307+
string,
308+
Array<{
309+
provider: string
310+
model: string
311+
inputTokens: number
312+
outputTokens: number
313+
cacheWriteTokens: number
314+
cacheReadTokens: number
315+
}>
316+
>()
281317
for (const row of inputTokensByTaskId) {
282318
const existing = inputTokensLookup.get(row.taskId) ?? []
283-
existing.push({ provider: row.provider, model: row.model, inputTokens: row.inputTokens })
319+
existing.push({
320+
provider: row.provider,
321+
model: row.model,
322+
inputTokens: row.inputTokens,
323+
outputTokens: row.outputTokens ?? 0,
324+
cacheWriteTokens: row.cacheWriteTokens ?? 0,
325+
cacheReadTokens: row.cacheReadTokens ?? 0,
326+
})
284327
inputTokensLookup.set(row.taskId, existing)
285328
}
286329

@@ -301,7 +344,39 @@ function summarizeSubtreeUsage(
301344
continue
302345
}
303346

304-
totalCost += usage.totalCost
347+
// When customPricing is present and usage has events, recompute cost
348+
// if stored usage.totalCost === 0 or if customPricing contains pricing for any model in the task.
349+
if (customPricing && customPricing.size > 0 && usage.eventCount > 0) {
350+
const perModelRows = inputTokensLookup.get(taskId)
351+
if (perModelRows && perModelRows.length > 0) {
352+
let taskRecomputedCost = 0
353+
let hasRecomputedCost = false
354+
for (const row of perModelRows) {
355+
const hasCustom = customPricing.has(customPricingKey(row.provider, row.model))
356+
if (hasCustom || usage.totalCost === 0) {
357+
taskRecomputedCost += computeCostFromAggregated(
358+
row.provider,
359+
row.model,
360+
row.inputTokens,
361+
row.outputTokens,
362+
row.cacheWriteTokens,
363+
row.cacheReadTokens,
364+
customPricing,
365+
)
366+
hasRecomputedCost = true
367+
}
368+
}
369+
if (hasRecomputedCost) {
370+
totalCost += taskRecomputedCost
371+
} else {
372+
totalCost += usage.totalCost
373+
}
374+
} else {
375+
totalCost += usage.totalCost
376+
}
377+
} else {
378+
totalCost += usage.totalCost
379+
}
305380

306381
// When customPricing is available and the stored cacheDiscountBase
307382
// is 0, recompute it from per-(provider, model) input token sums.

‎src/services/stats/UsageStatsDatabase.ts‎

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3267,36 +3267,115 @@ export class UsageStatsDatabase {
32673267
}
32683268

32693269
/**
3270-
* Queries per-(task_id, provider, model) input token sums from
3270+
* Queries per-(hour/day, provider, model) token sums from usage_events.
3271+
* Used for day-axis recomputation with custom pricing.
3272+
*
3273+
* @param fromEpochMs Start epoch ms (inclusive). 0 for no lower bound.
3274+
* @param toEpochMs End epoch ms (exclusive). MAX_SAFE_INTEGER for no upper bound.
3275+
* @param includeCancelled If true, includes cancelled events.
3276+
* @returns Array of { occurredAt, provider, model, endpoint, mode, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens } rows.
3277+
*/
3278+
queryDailyTokensByProviderModel(
3279+
fromEpochMs: number,
3280+
toEpochMs: number,
3281+
includeCancelled: boolean = false,
3282+
): Array<{
3283+
occurredAt: string
3284+
provider: string
3285+
model: string
3286+
endpoint: string | null
3287+
mode: string
3288+
inputTokens: number
3289+
outputTokens: number
3290+
cacheWriteTokens: number
3291+
cacheReadTokens: number
3292+
}> {
3293+
const db = this.getDb()
3294+
3295+
try {
3296+
let query = `SELECT occurred_at, provider, model, endpoint, mode,
3297+
SUM(COALESCE(json_extract(usage_json, '$.inputTokens.value'), 0)) as input_tokens,
3298+
SUM(COALESCE(json_extract(usage_json, '$.outputTokens.value'), 0)) as output_tokens,
3299+
SUM(COALESCE(json_extract(usage_json, '$.cacheWriteTokens.value'), 0)) as cache_write_tokens,
3300+
SUM(COALESCE(json_extract(usage_json, '$.cacheReadTokens.value'), 0)) as cache_read_tokens
3301+
FROM usage_events
3302+
WHERE occurred_epoch_ms >= ? AND occurred_epoch_ms < ?`
3303+
const params: Array<number | string> = [fromEpochMs, toEpochMs]
3304+
3305+
if (!includeCancelled) {
3306+
query += ` AND status != 'cancelled'`
3307+
}
3308+
3309+
query += ` GROUP BY SUBSTR(occurred_at, 1, 13), provider, model, endpoint, mode`
3310+
3311+
const rows = db.prepare(query).all(...params) as Array<Record<string, unknown>>
3312+
3313+
return rows.map((row) => ({
3314+
occurredAt: (row.occurred_at as string) ?? "",
3315+
provider: (row.provider as string) ?? "",
3316+
model: (row.model as string) ?? "",
3317+
endpoint: (row.endpoint as string | null) ?? null,
3318+
mode: (row.mode as string) ?? "",
3319+
inputTokens: (row.input_tokens as number) ?? 0,
3320+
outputTokens: (row.output_tokens as number) ?? 0,
3321+
cacheWriteTokens: (row.cache_write_tokens as number) ?? 0,
3322+
cacheReadTokens: (row.cache_read_tokens as number) ?? 0,
3323+
}))
3324+
} catch (err) {
3325+
throw new StatsDbError("STATS_DB/read/001", "Failed to query daily tokens by provider+model", err)
3326+
}
3327+
}
3328+
3329+
/**
3330+
* Queries per-(task_id, provider, model) token sums from
32713331
* usage_events for a given set of task IDs. Used by the task projection
3272-
* to recompute `cache_discount_base` at query time when `customPricing`
3273-
* is available, overriding the stale stored value (which was 0 for custom
3274-
* models because pricing wasn't available at write time).
3332+
* to recompute `totalCost` and `cache_discount_base` at query time when
3333+
* `customPricing` is available, overriding the stale stored values (which
3334+
* were 0 for custom models because pricing wasn't available at write time).
32753335
*
32763336
* @param taskIds The task IDs to query.
32773337
* @param rangeMs Optional time range filter.
32783338
* @param includeCancelled If true, includes cancelled events.
3279-
* @returns Array of { taskId, provider, model, inputTokens } rows.
3339+
* @returns Array of { taskId, provider, model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens } rows.
32803340
*/
32813341
queryInputTokensByTaskId(
32823342
taskIds: string[],
32833343
rangeMs?: StatsQueryRangeMs,
32843344
includeCancelled: boolean = false,
3285-
): Array<{ taskId: string; provider: string; model: string; inputTokens: number }> {
3345+
): Array<{
3346+
taskId: string
3347+
provider: string
3348+
model: string
3349+
inputTokens: number
3350+
outputTokens: number
3351+
cacheWriteTokens: number
3352+
cacheReadTokens: number
3353+
}> {
32863354
const db = this.getDb()
32873355
const uniqueTaskIds = [...new Set(taskIds)]
32883356

32893357
if (uniqueTaskIds.length === 0) return []
32903358

32913359
try {
3292-
const result: Array<{ taskId: string; provider: string; model: string; inputTokens: number }> = []
3360+
const result: Array<{
3361+
taskId: string
3362+
provider: string
3363+
model: string
3364+
inputTokens: number
3365+
outputTokens: number
3366+
cacheWriteTokens: number
3367+
cacheReadTokens: number
3368+
}> = []
32933369

32943370
for (let start = 0; start < uniqueTaskIds.length; start += TASK_ID_QUERY_CHUNK_SIZE) {
32953371
const chunk = uniqueTaskIds.slice(start, start + TASK_ID_QUERY_CHUNK_SIZE)
32963372
const placeholders = chunk.map(() => "?").join(", ")
32973373

32983374
let query = `SELECT task_id, provider, model,
3299-
SUM(COALESCE(json_extract(usage_json, '$.inputTokens.value'), 0)) as input_tokens
3375+
SUM(COALESCE(json_extract(usage_json, '$.inputTokens.value'), 0)) as input_tokens,
3376+
SUM(COALESCE(json_extract(usage_json, '$.outputTokens.value'), 0)) as output_tokens,
3377+
SUM(COALESCE(json_extract(usage_json, '$.cacheWriteTokens.value'), 0)) as cache_write_tokens,
3378+
SUM(COALESCE(json_extract(usage_json, '$.cacheReadTokens.value'), 0)) as cache_read_tokens
33003379
FROM usage_events
33013380
WHERE task_id IN (${placeholders})`
33023381
const params: Array<string | number> = [...chunk]
@@ -3324,6 +3403,9 @@ export class UsageStatsDatabase {
33243403
provider: (row.provider as string) ?? "",
33253404
model: (row.model as string) ?? "",
33263405
inputTokens: (row.input_tokens as number) ?? 0,
3406+
outputTokens: (row.output_tokens as number) ?? 0,
3407+
cacheWriteTokens: (row.cache_write_tokens as number) ?? 0,
3408+
cacheReadTokens: (row.cache_read_tokens as number) ?? 0,
33273409
})
33283410
}
33293411
}

0 commit comments

Comments
 (0)