diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ae374..e4170e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Token breakdown: decompose the "in" headline into fresh vs cached.** The headline + "NN in" is dominated by cheap `cache_read` tokens; a new `Input X new · Y cached` + line separates freshly-billed input (`input + cache_creation`) from cache reads, so the + cost line is interpretable (a big "in" at 97% cache is mostly $1.50/M reads, not $15/M + fresh input). Applied to the single-session and live views. + ## [1.0.0] - 2026-02-18 ### Added diff --git a/package-lock.json b/package-lock.json index 8b66636..b5aa8a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -186,7 +186,6 @@ "integrity": "sha512-PI+bOkutJnO7hsPZALHKWFj83K/45p4ABODUf0kKIoJS8tXJgDDhjYJGg6Eh/x177OokLfBPJUV9z3OspGt99g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.48.2", "@algolia/requester-browser-xhr": "5.48.2", @@ -1371,7 +1370,6 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1779,7 +1777,6 @@ "integrity": "sha512-U5U2dCy+ei/elHp7jMdYOPWBO0sjiUEyIW0PEblm4cL8GvDNiyiOlhZmKgwWbyPPj0w0s6HXeMZBAofTdVWgIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.14.2", "@algolia/client-abtesting": "5.48.2", @@ -2055,7 +2052,6 @@ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -2360,7 +2356,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2671,7 +2666,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2796,7 +2790,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3358,7 +3351,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3497,7 +3489,6 @@ "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", diff --git a/src/formatter.test.ts b/src/formatter.test.ts index e5e3453..0c029b3 100644 --- a/src/formatter.test.ts +++ b/src/formatter.test.ts @@ -413,3 +413,19 @@ describe('formatter: insights', () => { expect(output).toContain('Warmup overhead'); }); }); + +describe('formatSession: input decomposed into fresh vs cached', () => { + it('shows freshly-billed input (input + cache writes) and cache reads separately', () => { + // input 5K + cacheCreation 10K = 15K fresh; cacheRead 50K cached. + const output = strip(formatSession(makeAnalysis())); + expect(output).toContain('Input'); + expect(output).toContain('15.0K new'); + expect(output).toContain('50.0K cached'); + }); + + it('the headline "in" remains the full context total (fresh + cached)', () => { + // input+cacheRead+cacheCreation = 65K — the line a user sees as scale. + const output = strip(formatSession(makeAnalysis())); + expect(output).toContain('65.0K in'); + }); +}); diff --git a/src/formatter.ts b/src/formatter.ts index b226a59..43cefdf 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -162,6 +162,12 @@ export function formatSession(analysis: SessionAnalysis): string { const inBar = Math.round(inFrac * BAR_WIDTH); const outBar = BAR_WIDTH - inBar; lines.push(` Tokens ${chalk.cyan('\u2588'.repeat(inBar))}${chalk.green('\u2588'.repeat(outBar))} ${chalk.cyan(formatTokens(tokens.input + tokens.cacheRead + tokens.cacheCreation) + ' in')} ${chalk.green(formatTokens(tokens.output) + ' out')}`); + // Decompose "in": the headline is dominated by cheap cache reads. Show the + // freshly-billed input (input + cache writes) separately so the cost line + // is interpretable (a huge "in" at 97% cache is mostly $1.50/M reads, not + // $15/M fresh input). + const freshIn = tokens.input + tokens.cacheCreation; + lines.push(` Input ${chalk.gray(formatTokens(freshIn) + ' new')} ${chalk.gray('\u00b7')} ${chalk.gray(formatTokens(tokens.cacheRead) + ' cached')}`); } const cachePct = Math.round(analysis.cacheHitRate * 100); lines.push(` Cache ${renderCacheBar(analysis.cacheHitRate)} ${cachePct}% hit`); @@ -263,6 +269,8 @@ export function formatSessionLive(analysis: SessionAnalysis): string { const inBar = Math.round(inFrac * BAR_WIDTH); const outBar = BAR_WIDTH - inBar; lines.push(` Tokens ${chalk.cyan('\u2588'.repeat(inBar))}${chalk.green('\u2588'.repeat(outBar))} ${chalk.cyan(formatTokens(liveTok.input + liveTok.cacheRead + liveTok.cacheCreation) + ' in')} ${chalk.green(formatTokens(liveTok.output) + ' out')}`); + const liveFreshIn = liveTok.input + liveTok.cacheCreation; + lines.push(` Input ${chalk.gray(formatTokens(liveFreshIn) + ' new')} ${chalk.gray('\u00b7')} ${chalk.gray(formatTokens(liveTok.cacheRead) + ' cached')}`); } const liveCachePct = Math.round(analysis.cacheHitRate * 100); lines.push(` Cache ${renderCacheBar(analysis.cacheHitRate)} ${liveCachePct}% hit`);