diff --git a/apps/web/src/components/cloud-agent-next/ChatHeader.test.ts b/apps/web/src/components/cloud-agent-next/ChatHeader.test.ts index 7962d2e315..9391869f87 100644 --- a/apps/web/src/components/cloud-agent-next/ChatHeader.test.ts +++ b/apps/web/src/components/cloud-agent-next/ChatHeader.test.ts @@ -86,65 +86,65 @@ describe('SessionInfoDialog session costs', () => { }); expect(markup).toContain('Token Usage'); - expect(markup).toContain('$1.2346'); + expect(markup).toContain('$1.23'); expect(markup).toContain('Root session'); - expect(markup).toContain('$0.6543'); + expect(markup).toContain('$0.65'); expect(markup).toContain('Subagents'); - expect(markup).toContain('$0.3457'); + expect(markup).toContain('$0.35'); expect(markup).toContain('Older activity'); - expect(markup).toContain('$0.2346'); + expect(markup).toContain('$0.23'); expect(markup).not.toContain('Model cost'); }); - it('formats boundary total, root, and older-activity costs using integer microdollars', () => { + it('formats boundary total, root, and older-activity costs using integer cents', () => { const markup = renderSessionInfo({ - totalCostUsd: 0.05005, - rootCostUsd: 0.05, + totalCostUsd: 5.005, + rootCostUsd: 5, subagentCostUsd: 0, - olderActivityCostUsd: 0.00005, + olderActivityCostUsd: 0.005, }); expect(markup).toContain('Token Usage'); - expect(markup).toContain('$0.0501'); + expect(markup).toContain('$5.01'); expect(markup).toContain('Root session'); - expect(markup).toContain('$0.0500'); + expect(markup).toContain('$5.00'); expect(markup).toContain('Older activity'); - expect(markup).toContain('$0.0001'); + expect(markup).toContain('$0.01'); expect(markup).not.toContain('Subagents'); }); - it('renders a computed 50-microdollar subagent residual without floating-point loss', () => { + it('renders a computed half-cent subagent residual without floating-point loss', () => { const markup = renderSessionInfo({ - totalCostUsd: 0.10005, + totalCostUsd: 0.105, rootCostUsd: 0.1, - subagentCostUsd: 0.10005 - 0.1, + subagentCostUsd: 0.105 - 0.1, olderActivityCostUsd: 0, }); expect(markup).toContain('Token Usage'); - expect(markup).toContain('$0.1001'); + expect(markup).toContain('$0.11'); expect(markup).toContain('Root session'); - expect(markup).toContain('$0.1000'); + expect(markup).toContain('$0.10'); expect(markup).toContain('Subagents'); - expect(markup).toContain('$0.0001'); + expect(markup).toContain('$0.01'); expect(markup).not.toContain('Older activity'); }); it('keeps rounded component rows reconciled to the displayed model cost', () => { const markup = renderSessionInfo({ - totalCostUsd: 0.0001, - rootCostUsd: 0.00005, - subagentCostUsd: 0.00005, + totalCostUsd: 0.01, + rootCostUsd: 0.005, + subagentCostUsd: 0.005, olderActivityCostUsd: 0, }); expect(markup).toContain('Token Usage'); expect(markup).toContain('Root session'); expect(markup).toContain('Subagents'); - expect([...markup.matchAll(/\$(\d+\.\d{4})/g)].map(match => match[1])).toEqual([ - '0.0001', - '0.0000', - '0.0001', + expect([...markup.matchAll(/\$(\d+\.\d{2})/g)].map(match => match[1])).toEqual([ + '0.01', + '0.00', + '0.01', ]); }); @@ -157,16 +157,16 @@ describe('SessionInfoDialog session costs', () => { }); expect(markup).toContain('Root session'); - expect(markup).toContain('$0.2500'); + expect(markup).toContain('$0.25'); expect(markup).not.toContain('Subagents'); expect(markup).not.toContain('Older activity'); }); it.each([ - { microdollars: 49, shouldRender: false }, - { microdollars: 50, shouldRender: true }, + { microdollars: 4_999, shouldRender: false }, + { microdollars: 5_000, shouldRender: true }, ])( - 'applies the four-decimal display threshold to a $microdollars-microdollar subagent residual', + 'applies the two-decimal display threshold to a $microdollars-microdollar subagent residual', ({ microdollars, shouldRender }) => { const subagentCostUsd = microdollars / 1_000_000; const markup = renderSessionInfo({ @@ -180,19 +180,19 @@ describe('SessionInfoDialog session costs', () => { if (shouldRender) { expect(markup).toContain('Subagents'); - expect(markup).toContain('$0.0001'); + expect(markup).toContain('$0.01'); } else { expect(markup).not.toContain('Subagents'); - expect(markup).not.toContain('$0.0000'); + expect(markup).not.toContain('$0.00'); } } ); it.each([ - { microdollars: 49, shouldRender: false }, - { microdollars: 50, shouldRender: true }, + { microdollars: 4_999, shouldRender: false }, + { microdollars: 5_000, shouldRender: true }, ])( - 'applies the four-decimal display threshold to a $microdollars-microdollar older-activity residual', + 'applies the two-decimal display threshold to a $microdollars-microdollar older-activity residual', ({ microdollars, shouldRender }) => { const olderActivityCostUsd = microdollars / 1_000_000; const markup = renderSessionInfo({ @@ -206,52 +206,52 @@ describe('SessionInfoDialog session costs', () => { if (shouldRender) { expect(markup).toContain('Older activity'); - expect(markup).toContain('$0.0001'); + expect(markup).toContain('$0.01'); } else { expect(markup).not.toContain('Older activity'); - expect(markup).not.toContain('$0.0000'); + expect(markup).not.toContain('$0.00'); } } ); it('keeps a root-only reconciliation visible when the total reaches display precision', () => { const markup = renderSessionInfo({ - totalCostUsd: 0.00005, - rootCostUsd: 0.00005, + totalCostUsd: 0.005, + rootCostUsd: 0.005, subagentCostUsd: 0, olderActivityCostUsd: 0, }); expect(markup).toContain('Root session'); - expect(markup).toContain('$0.0001'); + expect(markup).toContain('$0.01'); expect(markup).not.toContain('Subagents'); expect(markup).not.toContain('Older activity'); }); it.each([ { - totalCostUsd: 0.000049, - rootCostUsd: 0.000049, + totalCostUsd: 0.004999, + rootCostUsd: 0.004999, subagentCostUsd: 0, olderActivityCostUsd: 0, }, { - totalCostUsd: 0.000049, + totalCostUsd: 0.004999, rootCostUsd: 0, - subagentCostUsd: 0.000049, + subagentCostUsd: 0.004999, olderActivityCostUsd: 0, }, { - totalCostUsd: 0.000049, + totalCostUsd: 0.004999, rootCostUsd: 0, subagentCostUsd: 0, - olderActivityCostUsd: 0.000049, + olderActivityCostUsd: 0.004999, }, ])('omits the total-cost section for totals below display precision', sessionCostBreakdown => { const markup = renderSessionInfo(sessionCostBreakdown); expect(markup).not.toContain('Token Usage'); - expect(markup).not.toContain('$0.0000'); + expect(markup).not.toContain('$0.00'); expect(markup).not.toContain('Root session'); expect(markup).not.toContain('Subagents'); expect(markup).not.toContain('Older activity'); @@ -267,7 +267,7 @@ describe('SessionInfoDialog session costs', () => { }); expect(markup).toContain('Root session'); - expect(markup).toContain('$0.0000'); + expect(markup).toContain('$0.00'); expect(markup).toContain('Subagents'); expect(markup).not.toContain('Older activity'); }); @@ -318,7 +318,7 @@ describe('SessionInfoDialog session costs', () => { }); expect(markup).toContain('Token Usage'); - expect(markup).toContain('$0.2500'); + expect(markup).toContain('$0.25'); expect(markup).not.toContain('Root session'); expect(markup).not.toContain('Subagents'); expect(markup).not.toContain('Older activity'); diff --git a/apps/web/src/components/cloud-agent-next/session-cost-breakdown.test.ts b/apps/web/src/components/cloud-agent-next/session-cost-breakdown.test.ts index c574d953be..ba2aaea840 100644 --- a/apps/web/src/components/cloud-agent-next/session-cost-breakdown.test.ts +++ b/apps/web/src/components/cloud-agent-next/session-cost-breakdown.test.ts @@ -64,8 +64,8 @@ function createTaskToolPart(): Part { } describe('isRenderableSessionCost', () => { - it.each([0.0000495, 0.00004999999999999, 0.00005, 0.00005000000000001, 0.01])( - 'accepts finite costs that round to at least 50 microdollars: %s', + it.each([0.0049995, 0.004999999999999, 0.005, 0.005000000000001, 0.01])( + 'accepts finite costs that round to at least half a cent: %s', costUsd => { expect(isRenderableSessionCost(costUsd)).toBe(true); } @@ -75,10 +75,10 @@ describe('isRenderableSessionCost', () => { Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, - -0.00005, + -0.005, 0, - 0.0000494, - 0.00004949999999999, + 0.0049994, + 0.004999499999999, ])('rejects non-finite or subthreshold costs: %s', costUsd => { expect(isRenderableSessionCost(costUsd)).toBe(false); }); @@ -86,22 +86,22 @@ describe('isRenderableSessionCost', () => { describe('formatSessionCost', () => { it.each([ - [0, '$0.0000'], - [0.0000494, '$0.0000'], - [0.0000495, '$0.0001'], - [0.00005, '$0.0001'], - [0.050049, '$0.0500'], - [0.05005, '$0.0501'], - [0.10005, '$0.1001'], - [1.23455, '$1.2346'], - ])('formats %s USD as %s using integer-microdollar half-up rounding', (costUsd, expected) => { + [0, '$0.00'], + [0.00494, '$0.00'], + [0.00495, '$0.00'], + [0.005, '$0.01'], + [0.0149, '$0.01'], + [0.015, '$0.02'], + [1.234, '$1.23'], + [1.235, '$1.24'], + ])('formats %s USD as %s using integer-cent half-up rounding', (costUsd, expected) => { expect(formatSessionCost(costUsd)).toBe(expected); }); it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, -0.01])( 'formats invalid or negative costs as zero: %s', costUsd => { - expect(formatSessionCost(costUsd)).toBe('$0.0000'); + expect(formatSessionCost(costUsd)).toBe('$0.00'); } ); }); @@ -110,15 +110,15 @@ describe('getDisplayedSessionCostBreakdown', () => { it('keeps half-unit root and subagent costs reconciled to the displayed total', () => { expect( getDisplayedSessionCostBreakdown({ - totalCostUsd: 0.0001, - rootCostUsd: 0.00005, - subagentCostUsd: 0.00005, + totalCostUsd: 0.01, + rootCostUsd: 0.005, + subagentCostUsd: 0.005, olderActivityCostUsd: 0, }) ).toEqual({ - totalCostUsd: 0.0001, + totalCostUsd: 0.01, rootCostUsd: 0, - subagentCostUsd: 0.0001, + subagentCostUsd: 0.01, olderActivityCostUsd: 0, }); }); @@ -126,15 +126,15 @@ describe('getDisplayedSessionCostBreakdown', () => { it('limits rounded residual rows to the displayed total', () => { expect( getDisplayedSessionCostBreakdown({ - totalCostUsd: 0.0001, + totalCostUsd: 0.01, rootCostUsd: 0, - subagentCostUsd: 0.00005, - olderActivityCostUsd: 0.00005, + subagentCostUsd: 0.005, + olderActivityCostUsd: 0.005, }) ).toEqual({ - totalCostUsd: 0.0001, + totalCostUsd: 0.01, rootCostUsd: 0, - subagentCostUsd: 0.0001, + subagentCostUsd: 0.01, olderActivityCostUsd: 0, }); }); @@ -148,10 +148,10 @@ describe('getDisplayedSessionCostBreakdown', () => { }); expect(displayed).toEqual({ - totalCostUsd: 1.2346, - rootCostUsd: 0.6543, - subagentCostUsd: 0.3457, - olderActivityCostUsd: 0.2346, + totalCostUsd: 1.23, + rootCostUsd: 0.65, + subagentCostUsd: 0.35, + olderActivityCostUsd: 0.23, }); expect( displayed.rootCostUsd + displayed.subagentCostUsd + displayed.olderActivityCostUsd @@ -230,17 +230,17 @@ describe('getSessionCostBreakdown', () => { expect(result.subagentCostUsd).toBeCloseTo(0.3, 12); }); - it('keeps an exact 50-microdollar folded subagent residual renderable', () => { + it('keeps an exact half-cent folded subagent residual renderable', () => { const result = getSessionCostBreakdown( - [createAssistantMessage(0.10005, [createStepFinishPart(0.1)])], + [createAssistantMessage(0.105, [createStepFinishPart(0.1)])], null, - 0.10005 + 0.105 ); - expect(result.subagentCostUsd).toBeLessThan(0.00005); - expect(result.subagentCostUsd).toBeCloseTo(0.00005, 12); + expect(result.subagentCostUsd).toBeLessThan(0.005); + expect(result.subagentCostUsd).toBeCloseTo(0.005, 12); expect(isRenderableSessionCost(result.subagentCostUsd)).toBe(true); - expect(formatSessionCost(result.subagentCostUsd)).toBe('$0.0001'); + expect(formatSessionCost(result.subagentCostUsd)).toBe('$0.01'); }); it('combines multiple assistant messages and multiple root steps', () => { @@ -455,39 +455,39 @@ describe('getSessionCostBreakdown', () => { expect(getSessionCostBreakdown(messages, null, 0.2).subagentCostUsd).toBe(0); }); - it.each([0.5, 1, 2, 49])( - 'suppresses older activity below the four-decimal rendering threshold: %s microdollars', + it.each([50, 100, 200, 4_999])( + 'suppresses older activity below the two-decimal rendering threshold: %s microdollars', persistedMicrodollars => { expect(getSessionCostBreakdown([], persistedMicrodollars, 0).olderActivityCostUsd).toBe(0); } ); - it('retains older activity at the four-decimal rendering threshold', () => { - const result = getSessionCostBreakdown([], 50, 0); + it('retains older activity at the two-decimal rendering threshold', () => { + const result = getSessionCostBreakdown([], 5_000, 0); - expect(result.totalCostUsd).toBe(0.00005); - expect(result.olderActivityCostUsd).toBe(0.00005); + expect(result.totalCostUsd).toBe(0.005); + expect(result.olderActivityCostUsd).toBe(0.005); }); - it('retains an exact 50-microdollar older-activity residual with nonzero live cost', () => { - const result = getSessionCostBreakdown([createAssistantMessage(0.1)], 100_050, 0.1); + it('retains an exact half-cent older-activity residual with nonzero live cost', () => { + const result = getSessionCostBreakdown([createAssistantMessage(0.1)], 105_000, 0.1); - expect(result.totalCostUsd).toBe(0.10005); - expect(result.olderActivityCostUsd).toBe(0.00005); + expect(result.totalCostUsd).toBe(0.105); + expect(result.olderActivityCostUsd).toBe(0.005); }); - it('suppresses a 49-microdollar older-activity residual with nonzero live cost', () => { - const result = getSessionCostBreakdown([createAssistantMessage(0.1)], 100_049, 0.1); + it('suppresses a 4999-microdollar older-activity residual with nonzero live cost', () => { + const result = getSessionCostBreakdown([createAssistantMessage(0.1)], 104_999, 0.1); - expect(result.totalCostUsd).toBe(0.100049); + expect(result.totalCostUsd).toBe(0.104999); expect(result.olderActivityCostUsd).toBe(0); }); - it('retains older activity above the four-decimal rendering threshold', () => { - const result = getSessionCostBreakdown([createAssistantMessage(0.05)], 50_200, 0.05); + it('retains older activity above the two-decimal rendering threshold', () => { + const result = getSessionCostBreakdown([createAssistantMessage(0.05)], 70_000, 0.05); - expect(result.totalCostUsd).toBe(0.0502); - expect(result.olderActivityCostUsd).toBeCloseTo(0.0002, 12); + expect(result.totalCostUsd).toBe(0.07); + expect(result.olderActivityCostUsd).toBeCloseTo(0.02, 12); }); it('excludes user messages even when their parts contain step-finish costs', () => { diff --git a/apps/web/src/components/cloud-agent-next/session-cost-breakdown.ts b/apps/web/src/components/cloud-agent-next/session-cost-breakdown.ts index 1874adea7a..13e0ebf558 100644 --- a/apps/web/src/components/cloud-agent-next/session-cost-breakdown.ts +++ b/apps/web/src/components/cloud-agent-next/session-cost-breakdown.ts @@ -1,7 +1,9 @@ import type { StoredMessage } from '@kilocode/cloud-agent-sdk'; const COST_RECONCILIATION_EPSILON_USD = 1e-6; -const MINIMUM_RENDERABLE_COST_MICRODOLLARS = 50; +const MICRODOLLARS_PER_DISPLAY_UNIT = 10_000; +const DISPLAY_UNITS_PER_DOLLAR = 100; +const MINIMUM_RENDERABLE_COST_MICRODOLLARS = MICRODOLLARS_PER_DISPLAY_UNIT / 2; export type SessionCostBreakdown = { totalCostUsd: number; @@ -17,11 +19,11 @@ export function isRenderableSessionCost(costUsd: number): boolean { function getDisplayedSessionCostUnits(costUsd: number): number { const costMicrodollars = Math.round(sanitizeNonNegativeCost(costUsd) * 1_000_000); - return Math.round(costMicrodollars / 100); + return Math.round(costMicrodollars / MICRODOLLARS_PER_DISPLAY_UNIT); } export function formatSessionCost(costUsd: number): string { - return `$${(getDisplayedSessionCostUnits(costUsd) / 10_000).toFixed(4)}`; + return `$${(getDisplayedSessionCostUnits(costUsd) / DISPLAY_UNITS_PER_DOLLAR).toFixed(2)}`; } export function getDisplayedSessionCostBreakdown( @@ -38,10 +40,10 @@ export function getDisplayedSessionCostBreakdown( ); return { - totalCostUsd: totalUnits / 10_000, - rootCostUsd: (totalUnits - subagentUnits - olderActivityUnits) / 10_000, - subagentCostUsd: subagentUnits / 10_000, - olderActivityCostUsd: olderActivityUnits / 10_000, + totalCostUsd: totalUnits / DISPLAY_UNITS_PER_DOLLAR, + rootCostUsd: (totalUnits - subagentUnits - olderActivityUnits) / DISPLAY_UNITS_PER_DOLLAR, + subagentCostUsd: subagentUnits / DISPLAY_UNITS_PER_DOLLAR, + olderActivityCostUsd: olderActivityUnits / DISPLAY_UNITS_PER_DOLLAR, }; }