Skip to content

Commit 90b874d

Browse files
authored
Merge pull request #80 from rajbos/token-calc
Add local debugging steps and improve token usage summary
2 parents 32e0bb1 + b29904d commit 90b874d

File tree

2 files changed

+93
-34
lines changed

2 files changed

+93
-34
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,33 @@ This caching significantly reduces the time needed for periodic updates, especia
7171
npx vsce package
7272
```
7373

74+
### Running the Extension Locally
75+
76+
To test and debug the extension in a local VS Code environment:
77+
78+
1. Install dependencies:
79+
```bash
80+
npm install
81+
```
82+
83+
2. Start watch mode (automatically recompiles on file changes):
84+
```bash
85+
npm run watch
86+
```
87+
88+
3. In VS Code, press **F5** to launch the Extension Development Host
89+
- This opens a new VS Code window with the extension running
90+
- The original window shows debug output and allows you to set breakpoints
91+
92+
4. In the Extension Development Host window:
93+
- The extension will be active and you'll see the token tracker in the status bar
94+
- Any changes you make to the code will be automatically compiled (thanks to watch mode)
95+
- Reload the Extension Development Host window (Ctrl+R or Cmd+R) to see your changes
96+
97+
5. To view console logs and debug information:
98+
- In the Extension Development Host window, open Developer Tools: **Help > Toggle Developer Tools**
99+
- Check the Console tab for any `console.log` output from the extension
100+
74101
### Available Scripts
75102

76103
- `npm run lint` - Run ESLint

src/extension.ts

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ interface TokenUsageStats {
1212
}
1313

1414
interface ModelUsage {
15-
[modelName: string]: number;
15+
[modelName: string]: {
16+
inputTokens: number;
17+
outputTokens: number;
18+
};
1619
}
1720

1821
interface ModelPricing {
@@ -56,6 +59,11 @@ interface SessionFileCache {
5659

5760
class CopilotTokenTracker implements vscode.Disposable {
5861
private statusBarItem: vscode.StatusBarItem;
62+
63+
// Helper method to get total tokens from ModelUsage
64+
private getTotalTokensFromModelUsage(modelUsage: ModelUsage): number {
65+
return Object.values(modelUsage).reduce((sum, usage) => sum + usage.inputTokens + usage.outputTokens, 0);
66+
}
5967
private updateInterval: NodeJS.Timeout | undefined;
6068
private initialDelayTimeout: NodeJS.Timeout | undefined;
6169
private detailsPanel: vscode.WebviewPanel | undefined;
@@ -174,7 +182,7 @@ class CopilotTokenTracker implements vscode.Disposable {
174182

175183
if (extensionsExistButInactive) {
176184
// Use shorter delay for testing in Codespaces
177-
const delaySeconds = process.env.CODESPACES === 'true' ? 10 : 60;
185+
const delaySeconds = process.env.CODESPACES === 'true' ? 10 : 15;
178186
this.log(`Copilot extensions found but not active yet - delaying initial update by ${delaySeconds} seconds to allow extensions to load`);
179187
this.log(`Setting timeout for ${new Date(Date.now() + (delaySeconds * 1000)).toLocaleTimeString()}`);
180188

@@ -251,7 +259,7 @@ class CopilotTokenTracker implements vscode.Disposable {
251259
tooltip.appendMarkdown(`**Avg Interactions/Session:** ${detailedStats.month.avgInteractionsPerSession}\n\n`);
252260
tooltip.appendMarkdown(`**Avg Tokens/Session:** ${detailedStats.month.avgTokensPerSession.toLocaleString()}\n\n`);
253261
tooltip.appendMarkdown('---\n\n');
254-
tooltip.appendMarkdown('*Cost estimates based on OpenAI/Anthropic API pricing*\n\n');
262+
tooltip.appendMarkdown('*Cost estimates based on actual input/output token ratios*\n\n');
255263
tooltip.appendMarkdown('*Updates automatically every 5 minutes*');
256264

257265
this.statusBarItem.tooltip = tooltip;
@@ -358,8 +366,12 @@ class CopilotTokenTracker implements vscode.Disposable {
358366
monthStats.interactions += interactions;
359367

360368
// Add model usage to month stats
361-
for (const [model, modelTokens] of Object.entries(modelUsage)) {
362-
monthStats.modelUsage[model] = (monthStats.modelUsage[model] || 0) + (modelTokens as number);
369+
for (const [model, usage] of Object.entries(modelUsage)) {
370+
if (!monthStats.modelUsage[model]) {
371+
monthStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
372+
}
373+
monthStats.modelUsage[model].inputTokens += usage.inputTokens;
374+
monthStats.modelUsage[model].outputTokens += usage.outputTokens;
363375
}
364376

365377
if (fileStats.mtime >= todayStart) {
@@ -368,8 +380,12 @@ class CopilotTokenTracker implements vscode.Disposable {
368380
todayStats.interactions += interactions;
369381

370382
// Add model usage to today stats
371-
for (const [model, modelTokens] of Object.entries(modelUsage)) {
372-
todayStats.modelUsage[model] = (todayStats.modelUsage[model] || 0) + (modelTokens as number);
383+
for (const [model, usage] of Object.entries(modelUsage)) {
384+
if (!todayStats.modelUsage[model]) {
385+
todayStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
386+
}
387+
todayStats.modelUsage[model].inputTokens += usage.inputTokens;
388+
todayStats.modelUsage[model].outputTokens += usage.outputTokens;
373389
}
374390
}
375391
}
@@ -452,22 +468,27 @@ class CopilotTokenTracker implements vscode.Disposable {
452468
// Get model for this request
453469
const model = this.getModelFromRequest(request);
454470

455-
// Estimate tokens from user message
471+
// Initialize model if not exists
472+
if (!modelUsage[model]) {
473+
modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
474+
}
475+
476+
// Estimate tokens from user message (input)
456477
if (request.message && request.message.parts) {
457478
for (const part of request.message.parts) {
458479
if (part.text) {
459480
const tokens = this.estimateTokensFromText(part.text, model);
460-
modelUsage[model] = (modelUsage[model] || 0) + tokens;
481+
modelUsage[model].inputTokens += tokens;
461482
}
462483
}
463484
}
464485

465-
// Estimate tokens from assistant response
486+
// Estimate tokens from assistant response (output)
466487
if (request.response && Array.isArray(request.response)) {
467488
for (const responseItem of request.response) {
468489
if (responseItem.value) {
469490
const tokens = this.estimateTokensFromText(responseItem.value, model);
470-
modelUsage[model] = (modelUsage[model] || 0) + tokens;
491+
modelUsage[model].outputTokens += tokens;
471492
}
472493
}
473494
}
@@ -528,27 +549,21 @@ class CopilotTokenTracker implements vscode.Disposable {
528549
private calculateEstimatedCost(modelUsage: ModelUsage): number {
529550
let totalCost = 0;
530551

531-
for (const [model, tokens] of Object.entries(modelUsage)) {
552+
for (const [model, usage] of Object.entries(modelUsage)) {
532553
const pricing = this.modelPricing[model];
533554

534555
if (pricing) {
535-
// Assume 50/50 split between input and output tokens
536-
// This is a simplification since we don't track them separately
537-
const inputTokens = tokens * 0.5;
538-
const outputTokens = tokens * 0.5;
539-
540-
const inputCost = (inputTokens / 1_000_000) * pricing.inputCostPerMillion;
541-
const outputCost = (outputTokens / 1_000_000) * pricing.outputCostPerMillion;
556+
// Use actual input and output token counts
557+
const inputCost = (usage.inputTokens / 1_000_000) * pricing.inputCostPerMillion;
558+
const outputCost = (usage.outputTokens / 1_000_000) * pricing.outputCostPerMillion;
542559

543560
totalCost += inputCost + outputCost;
544561
} else {
545562
// Fallback for models without pricing data - use GPT-4o-mini as default
546563
const fallbackPricing = this.modelPricing['gpt-4o-mini'];
547-
const inputTokens = tokens * 0.5;
548-
const outputTokens = tokens * 0.5;
549564

550-
const inputCost = (inputTokens / 1_000_000) * fallbackPricing.inputCostPerMillion;
551-
const outputCost = (outputTokens / 1_000_000) * fallbackPricing.outputCostPerMillion;
565+
const inputCost = (usage.inputTokens / 1_000_000) * fallbackPricing.inputCostPerMillion;
566+
const outputCost = (usage.outputTokens / 1_000_000) * fallbackPricing.outputCostPerMillion;
552567

553568
totalCost += inputCost + outputCost;
554569

@@ -790,31 +805,32 @@ class CopilotTokenTracker implements vscode.Disposable {
790805
private async estimateTokensFromSession(sessionFilePath: string): Promise<number> {
791806
try {
792807
const sessionContent = JSON.parse(fs.readFileSync(sessionFilePath, 'utf8'));
793-
let totalTokens = 0;
808+
let totalInputTokens = 0;
809+
let totalOutputTokens = 0;
794810

795811
if (sessionContent.requests && Array.isArray(sessionContent.requests)) {
796812
for (const request of sessionContent.requests) {
797-
// Estimate tokens from user message
813+
// Estimate tokens from user message (input)
798814
if (request.message && request.message.parts) {
799815
for (const part of request.message.parts) {
800816
if (part.text) {
801-
totalTokens += this.estimateTokensFromText(part.text);
817+
totalInputTokens += this.estimateTokensFromText(part.text);
802818
}
803819
}
804820
}
805821

806-
// Estimate tokens from assistant response
822+
// Estimate tokens from assistant response (output)
807823
if (request.response && Array.isArray(request.response)) {
808824
for (const responseItem of request.response) {
809825
if (responseItem.value) {
810-
totalTokens += this.estimateTokensFromText(responseItem.value, this.getModelFromRequest(request));
826+
totalOutputTokens += this.estimateTokensFromText(responseItem.value, this.getModelFromRequest(request));
811827
}
812828
}
813829
}
814830
}
815831
}
816832

817-
return totalTokens;
833+
return totalInputTokens + totalOutputTokens;
818834
} catch (error) {
819835
this.warn(`Error parsing session file ${sessionFilePath}: ${error}`);
820836
return 0;
@@ -1170,7 +1186,7 @@ class CopilotTokenTracker implements vscode.Disposable {
11701186
Token counts are estimated based on character count. CO₂, tree equivalents, water usage, and costs are derived from these token estimates.
11711187
</p>
11721188
<ul style="font-size: 12px; color: #b3b3b3; padding-left: 20px; list-style-position: inside; margin-top: 8px;">
1173-
<li><b>Cost Estimate:</b> Based on OpenAI API pricing (as of Dec 2025, <a href="https://openai.com/api/pricing/" style="color: #3794ff;">openai.com/api/pricing</a>) and standard Anthropic rates. Assumes 50/50 split between input and output tokens. <b>Note:</b> GitHub Copilot pricing may differ from direct API usage. These are reference estimates only.</li>
1189+
<li><b>Cost Estimate:</b> Based on public API pricing (see <a href="https://github.com/rajbos/github-copilot-token-usage/blob/main/src/modelPricing.json" style="color: #3794ff;">modelPricing.json</a> for sources and rates). Uses actual input/output token counts for accurate cost calculation. <b>Note:</b> GitHub Copilot pricing may differ from direct API usage. These are reference estimates only.</li>
11741190
<li><b>CO₂ Estimate:</b> Based on ~${this.co2Per1kTokens}g of CO₂e per 1,000 tokens.</li>
11751191
<li><b>Tree Equivalent:</b> Represents the fraction of a single mature tree's annual CO₂ absorption (~${(this.co2AbsorptionPerTreePerYear / 1000).toFixed(1)} kg/year).</li>
11761192
<li><b>Water Estimate:</b> Based on ~${this.waterUsagePer1kTokens}L of water per 1,000 tokens for data center cooling and operations.</li>
@@ -1226,17 +1242,33 @@ class CopilotTokenTracker implements vscode.Disposable {
12261242
const modelRows = Array.from(allModels).map(model => {
12271243
const ratio = this.tokenEstimators[model] || 0.25;
12281244
const charsPerToken = (1 / ratio).toFixed(1);
1229-
const monthlyTokens = stats.month.modelUsage[model] || 0;
1230-
const projectedTokens = calculateProjection(monthlyTokens);
1245+
1246+
const todayUsage = stats.today.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
1247+
const monthUsage = stats.month.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
1248+
1249+
const todayTotal = todayUsage.inputTokens + todayUsage.outputTokens;
1250+
const monthTotal = monthUsage.inputTokens + monthUsage.outputTokens;
1251+
const projectedTokens = calculateProjection(monthTotal);
1252+
1253+
const todayInputPercent = todayTotal > 0 ? ((todayUsage.inputTokens / todayTotal) * 100).toFixed(0) : 0;
1254+
const todayOutputPercent = todayTotal > 0 ? ((todayUsage.outputTokens / todayTotal) * 100).toFixed(0) : 0;
1255+
const monthInputPercent = monthTotal > 0 ? ((monthUsage.inputTokens / monthTotal) * 100).toFixed(0) : 0;
1256+
const monthOutputPercent = monthTotal > 0 ? ((monthUsage.outputTokens / monthTotal) * 100).toFixed(0) : 0;
12311257

12321258
return `
12331259
<tr>
12341260
<td class="metric-label">
12351261
${this.getModelDisplayName(model)}
12361262
<span style="font-size: 11px; color: #a0a0a0; font-weight: normal;">(~${charsPerToken} chars/tk)</span>
12371263
</td>
1238-
<td class="today-value">${(stats.today.modelUsage[model] || 0).toLocaleString()}</td>
1239-
<td class="month-value">${monthlyTokens.toLocaleString()}</td>
1264+
<td class="today-value">
1265+
${todayTotal.toLocaleString()}
1266+
<div style="font-size: 10px; color: #999; font-weight: normal; margin-top: 2px;">↑${todayInputPercent}% ↓${todayOutputPercent}%</div>
1267+
</td>
1268+
<td class="month-value">
1269+
${monthTotal.toLocaleString()}
1270+
<div style="font-size: 10px; color: #999; font-weight: normal; margin-top: 2px;">↑${monthInputPercent}% ↓${monthOutputPercent}%</div>
1271+
</td>
12401272
<td class="month-value">${Math.round(projectedTokens).toLocaleString()}</td>
12411273
</tr>
12421274
`;

0 commit comments

Comments
 (0)