mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-08 03:07:53 +00:00
Add task cost breakdown utilities
This commit is contained in:
parent
e18382d9ff
commit
6b3ec95ce3
3 changed files with 183 additions and 13 deletions
|
|
@ -21,6 +21,7 @@ import { getModelMaxOutputTokens } from "@roo/api"
|
|||
import { findLastIndex } from "@roo/array"
|
||||
|
||||
import { formatLargeNumber } from "@src/utils/format"
|
||||
import { getTaskHeaderCostTooltipData } from "@src/utils/taskCostBreakdown"
|
||||
import { cn } from "@src/lib/utils"
|
||||
import { StandardTooltip, Button } from "@src/components/ui"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
|
|
@ -129,6 +130,37 @@ const TaskHeader = ({
|
|||
|
||||
const hasTodos = todos && Array.isArray(todos) && todos.length > 0
|
||||
|
||||
const subtaskCosts = useMemo(() => {
|
||||
if (!subtaskDetails || subtaskDetails.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return subtaskDetails
|
||||
.map((subtask) => subtask.cost)
|
||||
.filter((cost): cost is number => typeof cost === "number" && Number.isFinite(cost))
|
||||
}, [subtaskDetails])
|
||||
|
||||
const tooltipCostData = useMemo(
|
||||
() =>
|
||||
getTaskHeaderCostTooltipData({
|
||||
ownCost: totalCost,
|
||||
aggregatedCost,
|
||||
hasSubtasksProp: hasSubtasks,
|
||||
costBreakdownProp: costBreakdown,
|
||||
subtaskCosts,
|
||||
labels: {
|
||||
own: t("common:costs.own"),
|
||||
subtasks: t("common:costs.subtasks"),
|
||||
},
|
||||
}),
|
||||
[totalCost, aggregatedCost, hasSubtasks, costBreakdown, subtaskCosts, t],
|
||||
)
|
||||
|
||||
const displayTotalCost = tooltipCostData.displayTotalCost
|
||||
const displayCostBreakdown = tooltipCostData.displayCostBreakdown
|
||||
const shouldTreatAsHasSubtasks = tooltipCostData.hasSubtasks
|
||||
const hasAnyCost = tooltipCostData.hasAnyCost
|
||||
|
||||
return (
|
||||
<div className="group pt-2 pb-0 px-3">
|
||||
{showLongRunningTaskMessage && !isTaskComplete && (
|
||||
|
|
@ -257,17 +289,19 @@ const TaskHeader = ({
|
|||
{formatLargeNumber(contextTokens || 0)} / {formatLargeNumber(contextWindow)}
|
||||
</span>
|
||||
</StandardTooltip>
|
||||
{!!totalCost && (
|
||||
{hasAnyCost && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
hasSubtasks ? (
|
||||
shouldTreatAsHasSubtasks ? (
|
||||
<div>
|
||||
<div>
|
||||
{t("chat:costs.totalWithSubtasks", {
|
||||
cost: (aggregatedCost ?? totalCost).toFixed(2),
|
||||
cost: displayTotalCost.toFixed(2),
|
||||
})}
|
||||
</div>
|
||||
{costBreakdown && <div className="text-xs mt-1">{costBreakdown}</div>}
|
||||
{displayCostBreakdown && (
|
||||
<div className="text-xs mt-1">{displayCostBreakdown}</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("chat:costs.total", { cost: totalCost.toFixed(2) })}</div>
|
||||
|
|
@ -276,8 +310,8 @@ const TaskHeader = ({
|
|||
side="top"
|
||||
sideOffset={8}>
|
||||
<span>
|
||||
${(aggregatedCost ?? totalCost).toFixed(2)}
|
||||
{hasSubtasks && (
|
||||
${displayTotalCost.toFixed(2)}
|
||||
{shouldTreatAsHasSubtasks && (
|
||||
<span className="text-xs ml-1" title={t("chat:costs.includesSubtasks")}>
|
||||
*
|
||||
</span>
|
||||
|
|
@ -416,7 +450,7 @@ const TaskHeader = ({
|
|||
</tr>
|
||||
)}
|
||||
|
||||
{!!totalCost && (
|
||||
{hasAnyCost && (
|
||||
<tr>
|
||||
<th className="font-medium text-left align-top w-1 whitespace-nowrap pr-3 h-[24px]">
|
||||
{t("chat:task.apiCost")}
|
||||
|
|
@ -424,15 +458,17 @@ const TaskHeader = ({
|
|||
<td className="font-light align-top">
|
||||
<StandardTooltip
|
||||
content={
|
||||
hasSubtasks ? (
|
||||
shouldTreatAsHasSubtasks ? (
|
||||
<div>
|
||||
<div>
|
||||
{t("chat:costs.totalWithSubtasks", {
|
||||
cost: (aggregatedCost ?? totalCost).toFixed(2),
|
||||
cost: displayTotalCost.toFixed(2),
|
||||
})}
|
||||
</div>
|
||||
{costBreakdown && (
|
||||
<div className="text-xs mt-1">{costBreakdown}</div>
|
||||
{displayCostBreakdown && (
|
||||
<div className="text-xs mt-1">
|
||||
{displayCostBreakdown}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -444,8 +480,8 @@ const TaskHeader = ({
|
|||
side="top"
|
||||
sideOffset={8}>
|
||||
<span>
|
||||
${(aggregatedCost ?? totalCost).toFixed(2)}
|
||||
{hasSubtasks && (
|
||||
${displayTotalCost.toFixed(2)}
|
||||
{shouldTreatAsHasSubtasks && (
|
||||
<span
|
||||
className="text-xs ml-1"
|
||||
title={t("chat:costs.includesSubtasks")}>
|
||||
|
|
|
|||
30
webview-ui/src/utils/__tests__/taskCostBreakdown.spec.ts
Normal file
30
webview-ui/src/utils/__tests__/taskCostBreakdown.spec.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
|
||||
import { computeTaskCostsIncludingSubtasks, getTaskHeaderCostTooltipData } from "@src/utils/taskCostBreakdown"
|
||||
|
||||
describe("taskCostBreakdown", () => {
|
||||
it("sums subtask line-item costs (micros) and produces a stable total", () => {
|
||||
const result = computeTaskCostsIncludingSubtasks(0.05, [0.16, 0.13])
|
||||
|
||||
expect(result.ownCostCents).toBe(5)
|
||||
expect(result.subtasksCostCents).toBe(29)
|
||||
expect(result.totalCostIncludingSubtasksCents).toBe(34)
|
||||
expect(result.totalCostIncludingSubtasks).toBeCloseTo(0.34, 10)
|
||||
})
|
||||
|
||||
it("prefers derived subtask sum over provided breakdown/aggregatedCost when details are available", () => {
|
||||
const data = getTaskHeaderCostTooltipData({
|
||||
ownCost: 0.05,
|
||||
aggregatedCost: 0.09,
|
||||
hasSubtasksProp: true,
|
||||
costBreakdownProp: "Own: $0.05 + Subtasks: $0.09",
|
||||
subtaskCosts: [0.16, 0.13],
|
||||
labels: { own: "Own", subtasks: "Subtasks" },
|
||||
})
|
||||
|
||||
expect(data.displayTotalCost).toBeCloseTo(0.34, 10)
|
||||
expect(data.displayCostBreakdown).toBe("Own: $0.05 + Subtasks: $0.29")
|
||||
expect(data.hasSubtasks).toBe(true)
|
||||
expect(data.hasAnyCost).toBe(true)
|
||||
})
|
||||
})
|
||||
104
webview-ui/src/utils/taskCostBreakdown.ts
Normal file
104
webview-ui/src/utils/taskCostBreakdown.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { formatCostBreakdown } from "@src/utils/costFormatting"
|
||||
|
||||
const MICROS_PER_DOLLAR = 1_000_000
|
||||
const MICROS_PER_CENT = 10_000
|
||||
|
||||
function dollarsToMicros(amount: number): number {
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(amount * MICROS_PER_DOLLAR)
|
||||
}
|
||||
|
||||
function microsToDollars(micros: number): number {
|
||||
return micros / MICROS_PER_DOLLAR
|
||||
}
|
||||
|
||||
export interface TaskCostsIncludingSubtasks {
|
||||
ownCost: number
|
||||
ownCostMicros: number
|
||||
ownCostCents: number
|
||||
|
||||
subtasksCost: number
|
||||
subtasksCostMicros: number
|
||||
subtasksCostCents: number
|
||||
|
||||
totalCostIncludingSubtasks: number
|
||||
totalCostIncludingSubtasksMicros: number
|
||||
totalCostIncludingSubtasksCents: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes task costs using integer micros for stable aggregation.
|
||||
*
|
||||
* Note: we only have access to floating-dollar amounts in the webview.
|
||||
* Converting to micros and summing avoids most floating point drift.
|
||||
*/
|
||||
export function computeTaskCostsIncludingSubtasks(ownCost: number, subtaskCosts: number[]): TaskCostsIncludingSubtasks {
|
||||
const ownCostMicros = dollarsToMicros(ownCost)
|
||||
const subtasksCostMicros = (subtaskCosts ?? []).reduce((sum, cost) => sum + dollarsToMicros(cost), 0)
|
||||
const totalCostIncludingSubtasksMicros = ownCostMicros + subtasksCostMicros
|
||||
|
||||
const ownCostCents = Math.round(ownCostMicros / MICROS_PER_CENT)
|
||||
const subtasksCostCents = Math.round(subtasksCostMicros / MICROS_PER_CENT)
|
||||
const totalCostIncludingSubtasksCents = Math.round(totalCostIncludingSubtasksMicros / MICROS_PER_CENT)
|
||||
|
||||
return {
|
||||
ownCost: microsToDollars(ownCostMicros),
|
||||
ownCostMicros,
|
||||
ownCostCents,
|
||||
subtasksCost: microsToDollars(subtasksCostMicros),
|
||||
subtasksCostMicros,
|
||||
subtasksCostCents,
|
||||
totalCostIncludingSubtasks: microsToDollars(totalCostIncludingSubtasksMicros),
|
||||
totalCostIncludingSubtasksMicros,
|
||||
totalCostIncludingSubtasksCents,
|
||||
}
|
||||
}
|
||||
|
||||
export interface TaskHeaderCostTooltipData {
|
||||
/** Total cost to display (includes subtasks when details provided). */
|
||||
displayTotalCost: number
|
||||
/** Breakdown string to show in tooltip, if subtasks exist. */
|
||||
displayCostBreakdown?: string
|
||||
/** Whether the UI should show the "includes subtasks" marker. */
|
||||
hasSubtasks: boolean
|
||||
/** Whether there is any cost to render. */
|
||||
hasAnyCost: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Cost display logic for TaskHeader tooltip.
|
||||
*
|
||||
* When subtask details are available, this derives subtasks cost as the sum of
|
||||
* subtask line-item totals (same source as the accordion list), rather than
|
||||
* trusting any derived deltas.
|
||||
*/
|
||||
export function getTaskHeaderCostTooltipData(params: {
|
||||
ownCost: number
|
||||
aggregatedCost?: number
|
||||
hasSubtasksProp?: boolean
|
||||
costBreakdownProp?: string
|
||||
subtaskCosts?: number[]
|
||||
labels: { own: string; subtasks: string }
|
||||
}): TaskHeaderCostTooltipData {
|
||||
const { ownCost, aggregatedCost, hasSubtasksProp, costBreakdownProp, subtaskCosts, labels } = params
|
||||
|
||||
const computed = computeTaskCostsIncludingSubtasks(ownCost, subtaskCosts ?? [])
|
||||
const hasComputedSubtasks = computed.subtasksCostCents > 0
|
||||
const hasSubtasks = !!hasSubtasksProp || hasComputedSubtasks
|
||||
|
||||
const displayTotalCost = hasComputedSubtasks ? computed.totalCostIncludingSubtasks : (aggregatedCost ?? ownCost)
|
||||
const displayCostBreakdown = hasComputedSubtasks
|
||||
? formatCostBreakdown(computed.ownCost, computed.subtasksCost, labels)
|
||||
: costBreakdownProp
|
||||
|
||||
const hasAnyCost = typeof displayTotalCost === "number" && Number.isFinite(displayTotalCost) && displayTotalCost > 0
|
||||
|
||||
return {
|
||||
displayTotalCost,
|
||||
displayCostBreakdown,
|
||||
hasSubtasks,
|
||||
hasAnyCost,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue