Skip to content

Commit 28d59c3

Browse files
k1ytmyk1yt
authored andcommitted
fix(stats): correct totalTokens calculation, provider pricing, and dashboard improvements
1 parent 28585aa commit 28d59c3

27 files changed

Lines changed: 1999 additions & 1736 deletions

packages/types/src/providers/qwen-code.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const qwenCodeModels = {
1010
contextWindow: 1_000_000,
1111
supportsImages: false,
1212
supportsPromptCache: false,
13-
inputPrice: 0,
14-
outputPrice: 0,
13+
inputPrice: 1.0,
14+
outputPrice: 5.0,
1515
cacheWritesPrice: 0,
1616
cacheReadsPrice: 0,
1717
description: "Qwen3 Coder Plus - High-performance coding model with 1M context window for large codebases",
@@ -21,8 +21,8 @@ export const qwenCodeModels = {
2121
contextWindow: 1_000_000,
2222
supportsImages: false,
2323
supportsPromptCache: false,
24-
inputPrice: 0,
25-
outputPrice: 0,
24+
inputPrice: 0.3,
25+
outputPrice: 1.5,
2626
cacheWritesPrice: 0,
2727
cacheReadsPrice: 0,
2828
description: "Qwen3 Coder Flash - Fast coding model with 1M context window optimized for speed",

src/api/providers/openai-codex.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
openAiCodexDefaultModelId,
99
OpenAiCodexModelId,
1010
openAiCodexModels,
11+
openAiNativeModels,
1112
type ReasoningEffort,
1213
type ReasoningEffortExtended,
1314
ApiProviderError,
@@ -16,6 +17,7 @@ import { TelemetryService } from "@roo-code/telemetry"
1617

1718
import { Package } from "../../shared/package"
1819
import type { ApiHandlerOptions } from "../../shared/api"
20+
import { calculateApiCostOpenAI } from "../../shared/cost"
1921

2022
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
2123
import { getModelParams } from "../transform/model-params"
@@ -126,15 +128,28 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
126128
? usage.output_tokens_details.reasoning_tokens
127129
: undefined
128130

129-
// Subscription-based: no per-token costs
131+
// Compute equivalent API cost using openAiNativeModels pricing.
132+
// The actual charge is covered by the ChatGPT Plus/Pro subscription,
133+
// but showing the equivalent API cost lets users compare usage value.
134+
const nativeModelInfo = openAiNativeModels[model.id as keyof typeof openAiNativeModels]
135+
const { totalCost } = nativeModelInfo
136+
? calculateApiCostOpenAI(
137+
nativeModelInfo,
138+
totalInputTokens,
139+
totalOutputTokens,
140+
cacheWriteTokens,
141+
cacheReadTokens,
142+
)
143+
: { totalCost: 0 }
144+
130145
const out: ApiStreamUsageChunk = {
131146
type: "usage",
132147
inputTokens: totalInputTokens,
133148
outputTokens: totalOutputTokens,
134149
cacheWriteTokens,
135150
cacheReadTokens,
136151
...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
137-
totalCost: 0, // Subscription-based pricing
152+
totalCost,
138153
}
139154
return out
140155
}

src/services/stats/UsageAggregator.ts

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ export class UsageAggregator {
7070
* @param query Statistics query
7171
* @param options Additional options (e.g. recordingPaused)
7272
*/
73-
query(
74-
events: UsageEventV1[],
75-
query: StatsQuery,
76-
options: { recordingPaused?: boolean } = {},
77-
): StatsSnapshot {
73+
query(events: UsageEventV1[], query: StatsQuery, options: { recordingPaused?: boolean } = {}): StatsSnapshot {
7874
// 1. Time range filtering
7975
const { from, to } = this.resolveTimeRange(query)
8076
const filtered = events.filter((event) => {
@@ -86,9 +82,7 @@ export class UsageAggregator {
8682

8783
// 2. Cancelled event filtering
8884
const includeCancelled = query.includeCancelled ?? false
89-
const visibleEvents = includeCancelled
90-
? filtered
91-
: filtered.filter((e) => e.status !== "cancelled")
85+
const visibleEvents = includeCancelled ? filtered : filtered.filter((e) => e.status !== "cancelled")
9286

9387
// 3. Compute bucket keys based on timezone
9488
const aggregatable: AggregatableEvent[] = visibleEvents.map((event) => {
@@ -341,10 +335,7 @@ export class UsageAggregator {
341335
* Returns the bucket key combinations for the groupBy axes from the event.
342336
* Up to 3 axes can be combined.
343337
*/
344-
private getGroupKeys(
345-
item: AggregatableEvent,
346-
groupBy: StatsQuery["groupBy"],
347-
): Record<string, string>[] {
338+
private getGroupKeys(item: AggregatableEvent, groupBy: StatsQuery["groupBy"]): Record<string, string>[] {
348339
if (groupBy.length === 0) {
349340
return [{}]
350341
}
@@ -397,33 +388,33 @@ export class UsageAggregator {
397388
case "status":
398389
return [event.status]
399390
case "source": {
400-
// Separate by the source of costUsd.
401-
// Feature 1: If the event has no costUsd but the cost can be
402-
// computed on-the-fly from model pricing, treat the source as
403-
// "estimated" (since it is derived, not provider-reported).
404-
const sources = new Set<string>()
405-
if (event.usage.costUsd) {
406-
sources.add(event.usage.costUsd.source)
407-
} else {
408-
// Check if cost can be computed; if so, mark as "estimated".
409-
// Otherwise the source remains "unknown".
410-
const computedCost = computeEventCost(event)
411-
if (computedCost > 0) {
412-
sources.add("estimated")
413-
}
391+
// Separate by the source of costUsd.
392+
// Feature 1: If the event has no costUsd but the cost can be
393+
// computed on-the-fly from model pricing, treat the source as
394+
// "estimated" (since it is derived, not provider-reported).
395+
const sources = new Set<string>()
396+
if (event.usage.costUsd) {
397+
sources.add(event.usage.costUsd.source)
398+
} else {
399+
// Check if cost can be computed; if so, mark as "estimated".
400+
// Otherwise the source remains "unknown".
401+
const computedCost = computeEventCost(event)
402+
if (computedCost > 0) {
403+
sources.add("estimated")
414404
}
415-
// Also consider the source of input/output tokens
416-
if (event.usage.inputTokens) {
417-
sources.add(event.usage.inputTokens.source)
418-
}
419-
if (event.usage.outputTokens) {
420-
sources.add(event.usage.outputTokens.source)
421-
}
422-
if (sources.size === 0) {
423-
sources.add("unknown")
424-
}
425-
return Array.from(sources)
426405
}
406+
// Also consider the source of input/output tokens
407+
if (event.usage.inputTokens) {
408+
sources.add(event.usage.inputTokens.source)
409+
}
410+
if (event.usage.outputTokens) {
411+
sources.add(event.usage.outputTokens.source)
412+
}
413+
if (sources.size === 0) {
414+
sources.add("unknown")
415+
}
416+
return Array.from(sources)
417+
}
427418
default:
428419
return []
429420
}
@@ -510,7 +501,9 @@ export class UsageAggregator {
510501
bucket.reasoningTokens += reasoningTokens
511502
}
512503

513-
bucket.totalTokens += totalTokens
504+
// Recompute from input + output (provider-neutral) to repair historical events
505+
// that may have been persisted with the old double-counted sum.
506+
bucket.totalTokens += inputTokens + outputTokens
514507
bucket.costUsd += costUsd
515508
}
516509

@@ -566,9 +559,7 @@ export class UsageAggregator {
566559
): StatsSnapshot["coverage"] {
567560
const times = visibleEvents.map((e) => new Date(e.event.occurredAt).getTime()).sort((a, b) => a - b)
568561

569-
const backfilledEventCount = visibleEvents.filter(
570-
(e) => e.event.provenance === "history-backfill",
571-
).length
562+
const backfilledEventCount = visibleEvents.filter((e) => e.event.provenance === "history-backfill").length
572563

573564
return {
574565
firstEventAt: times.length > 0 ? new Date(times[0]).toISOString() : undefined,

src/services/stats/UsageRecorder.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,8 @@ export class UsageRecorder {
9797
model: ctx.model,
9898
mode: ctx.mode,
9999
usage: {
100-
inputTokens:
101-
ctx.inputTokens > 0 ? { value: ctx.inputTokens, source: ctx.tokenSource } : undefined,
102-
outputTokens:
103-
ctx.outputTokens > 0 ? { value: ctx.outputTokens, source: ctx.tokenSource } : undefined,
100+
inputTokens: ctx.inputTokens > 0 ? { value: ctx.inputTokens, source: ctx.tokenSource } : undefined,
101+
outputTokens: ctx.outputTokens > 0 ? { value: ctx.outputTokens, source: ctx.tokenSource } : undefined,
104102
cacheWriteTokens: ctx.cacheWriteTokens
105103
? { value: ctx.cacheWriteTokens, source: ctx.tokenSource }
106104
: undefined,
@@ -110,19 +108,11 @@ export class UsageRecorder {
110108
reasoningTokens: ctx.reasoningTokens
111109
? { value: ctx.reasoningTokens, source: ctx.tokenSource }
112110
: undefined,
113-
// H3 fix: compute totalTokens at record time so aggregators/UI can rely on it.
114-
// Sum all token buckets. Inclusion semantics (whether cache/reasoning are already
115-
// counted inside input/output) are recorded in `semantics` below; the aggregator
116-
// is responsible for adjusting double-counting when semantics != "unknown".
117-
// Until provider-specific semantics are determined, we record the raw sum so the
118-
// total is never 0 (which previously broke heatmap/sort).
111+
// totalTokens = inputTokens + outputTokens (provider-neutral definition).
112+
// Cache tokens are a subset/breakdown of input; reasoning tokens are a subset of output.
113+
// Adding them separately would double-count. See docs/260720_22_gitignore-heatmap-fix/213200_debug-report.md
119114
totalTokens: {
120-
value:
121-
ctx.inputTokens +
122-
ctx.outputTokens +
123-
(ctx.cacheReadTokens ?? 0) +
124-
(ctx.cacheWriteTokens ?? 0) +
125-
(ctx.reasoningTokens ?? 0),
115+
value: ctx.inputTokens + ctx.outputTokens,
126116
source: ctx.tokenSource,
127117
},
128118
costUsd: ctx.totalCost ? { value: ctx.totalCost, source: ctx.costSource } : undefined,

0 commit comments

Comments
 (0)