From 7aaff4b55188c8caead9a2b07ebea9b9bcd6dc19 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sat, 27 Dec 2025 23:27:18 +0100 Subject: [PATCH 1/2] Add steps to debug locally --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index bc6a008e..fd5d4f66 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,33 @@ This caching significantly reduces the time needed for periodic updates, especia npx vsce package ``` +### Running the Extension Locally + +To test and debug the extension in a local VS Code environment: + +1. Install dependencies: + ```bash + npm install + ``` + +2. Start watch mode (automatically recompiles on file changes): + ```bash + npm run watch + ``` + +3. In VS Code, press **F5** to launch the Extension Development Host + - This opens a new VS Code window with the extension running + - The original window shows debug output and allows you to set breakpoints + +4. In the Extension Development Host window: + - The extension will be active and you'll see the token tracker in the status bar + - Any changes you make to the code will be automatically compiled (thanks to watch mode) + - Reload the Extension Development Host window (Ctrl+R or Cmd+R) to see your changes + +5. To view console logs and debug information: + - In the Extension Development Host window, open Developer Tools: **Help > Toggle Developer Tools** + - Check the Console tab for any `console.log` output from the extension + ### Available Scripts - `npm run lint` - Run ESLint From 2783a141e00f5853e339530bbafa12c8379eca97 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sat, 27 Dec 2025 23:39:16 +0100 Subject: [PATCH 2/2] Split summarization of token usage into input and ouput --- src/extension.ts | 100 +++++++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 97303e7a..439126d8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,7 +12,10 @@ interface TokenUsageStats { } interface ModelUsage { - [modelName: string]: number; + [modelName: string]: { + inputTokens: number; + outputTokens: number; + }; } interface ModelPricing { @@ -56,6 +59,11 @@ interface SessionFileCache { class CopilotTokenTracker implements vscode.Disposable { private statusBarItem: vscode.StatusBarItem; + + // Helper method to get total tokens from ModelUsage + private getTotalTokensFromModelUsage(modelUsage: ModelUsage): number { + return Object.values(modelUsage).reduce((sum, usage) => sum + usage.inputTokens + usage.outputTokens, 0); + } private updateInterval: NodeJS.Timeout | undefined; private initialDelayTimeout: NodeJS.Timeout | undefined; private detailsPanel: vscode.WebviewPanel | undefined; @@ -174,7 +182,7 @@ class CopilotTokenTracker implements vscode.Disposable { if (extensionsExistButInactive) { // Use shorter delay for testing in Codespaces - const delaySeconds = process.env.CODESPACES === 'true' ? 10 : 60; + const delaySeconds = process.env.CODESPACES === 'true' ? 10 : 15; this.log(`Copilot extensions found but not active yet - delaying initial update by ${delaySeconds} seconds to allow extensions to load`); this.log(`Setting timeout for ${new Date(Date.now() + (delaySeconds * 1000)).toLocaleTimeString()}`); @@ -251,7 +259,7 @@ class CopilotTokenTracker implements vscode.Disposable { tooltip.appendMarkdown(`**Avg Interactions/Session:** ${detailedStats.month.avgInteractionsPerSession}\n\n`); tooltip.appendMarkdown(`**Avg Tokens/Session:** ${detailedStats.month.avgTokensPerSession.toLocaleString()}\n\n`); tooltip.appendMarkdown('---\n\n'); - tooltip.appendMarkdown('*Cost estimates based on OpenAI/Anthropic API pricing*\n\n'); + tooltip.appendMarkdown('*Cost estimates based on actual input/output token ratios*\n\n'); tooltip.appendMarkdown('*Updates automatically every 5 minutes*'); this.statusBarItem.tooltip = tooltip; @@ -358,8 +366,12 @@ class CopilotTokenTracker implements vscode.Disposable { monthStats.interactions += interactions; // Add model usage to month stats - for (const [model, modelTokens] of Object.entries(modelUsage)) { - monthStats.modelUsage[model] = (monthStats.modelUsage[model] || 0) + (modelTokens as number); + for (const [model, usage] of Object.entries(modelUsage)) { + if (!monthStats.modelUsage[model]) { + monthStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 }; + } + monthStats.modelUsage[model].inputTokens += usage.inputTokens; + monthStats.modelUsage[model].outputTokens += usage.outputTokens; } if (fileStats.mtime >= todayStart) { @@ -368,8 +380,12 @@ class CopilotTokenTracker implements vscode.Disposable { todayStats.interactions += interactions; // Add model usage to today stats - for (const [model, modelTokens] of Object.entries(modelUsage)) { - todayStats.modelUsage[model] = (todayStats.modelUsage[model] || 0) + (modelTokens as number); + for (const [model, usage] of Object.entries(modelUsage)) { + if (!todayStats.modelUsage[model]) { + todayStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 }; + } + todayStats.modelUsage[model].inputTokens += usage.inputTokens; + todayStats.modelUsage[model].outputTokens += usage.outputTokens; } } } @@ -452,22 +468,27 @@ class CopilotTokenTracker implements vscode.Disposable { // Get model for this request const model = this.getModelFromRequest(request); - // Estimate tokens from user message + // Initialize model if not exists + if (!modelUsage[model]) { + modelUsage[model] = { inputTokens: 0, outputTokens: 0 }; + } + + // Estimate tokens from user message (input) if (request.message && request.message.parts) { for (const part of request.message.parts) { if (part.text) { const tokens = this.estimateTokensFromText(part.text, model); - modelUsage[model] = (modelUsage[model] || 0) + tokens; + modelUsage[model].inputTokens += tokens; } } } - // Estimate tokens from assistant response + // Estimate tokens from assistant response (output) if (request.response && Array.isArray(request.response)) { for (const responseItem of request.response) { if (responseItem.value) { const tokens = this.estimateTokensFromText(responseItem.value, model); - modelUsage[model] = (modelUsage[model] || 0) + tokens; + modelUsage[model].outputTokens += tokens; } } } @@ -528,27 +549,21 @@ class CopilotTokenTracker implements vscode.Disposable { private calculateEstimatedCost(modelUsage: ModelUsage): number { let totalCost = 0; - for (const [model, tokens] of Object.entries(modelUsage)) { + for (const [model, usage] of Object.entries(modelUsage)) { const pricing = this.modelPricing[model]; if (pricing) { - // Assume 50/50 split between input and output tokens - // This is a simplification since we don't track them separately - const inputTokens = tokens * 0.5; - const outputTokens = tokens * 0.5; - - const inputCost = (inputTokens / 1_000_000) * pricing.inputCostPerMillion; - const outputCost = (outputTokens / 1_000_000) * pricing.outputCostPerMillion; + // Use actual input and output token counts + const inputCost = (usage.inputTokens / 1_000_000) * pricing.inputCostPerMillion; + const outputCost = (usage.outputTokens / 1_000_000) * pricing.outputCostPerMillion; totalCost += inputCost + outputCost; } else { // Fallback for models without pricing data - use GPT-4o-mini as default const fallbackPricing = this.modelPricing['gpt-4o-mini']; - const inputTokens = tokens * 0.5; - const outputTokens = tokens * 0.5; - const inputCost = (inputTokens / 1_000_000) * fallbackPricing.inputCostPerMillion; - const outputCost = (outputTokens / 1_000_000) * fallbackPricing.outputCostPerMillion; + const inputCost = (usage.inputTokens / 1_000_000) * fallbackPricing.inputCostPerMillion; + const outputCost = (usage.outputTokens / 1_000_000) * fallbackPricing.outputCostPerMillion; totalCost += inputCost + outputCost; @@ -790,31 +805,32 @@ class CopilotTokenTracker implements vscode.Disposable { private async estimateTokensFromSession(sessionFilePath: string): Promise { try { const sessionContent = JSON.parse(fs.readFileSync(sessionFilePath, 'utf8')); - let totalTokens = 0; + let totalInputTokens = 0; + let totalOutputTokens = 0; if (sessionContent.requests && Array.isArray(sessionContent.requests)) { for (const request of sessionContent.requests) { - // Estimate tokens from user message + // Estimate tokens from user message (input) if (request.message && request.message.parts) { for (const part of request.message.parts) { if (part.text) { - totalTokens += this.estimateTokensFromText(part.text); + totalInputTokens += this.estimateTokensFromText(part.text); } } } - // Estimate tokens from assistant response + // Estimate tokens from assistant response (output) if (request.response && Array.isArray(request.response)) { for (const responseItem of request.response) { if (responseItem.value) { - totalTokens += this.estimateTokensFromText(responseItem.value, this.getModelFromRequest(request)); + totalOutputTokens += this.estimateTokensFromText(responseItem.value, this.getModelFromRequest(request)); } } } } } - return totalTokens; + return totalInputTokens + totalOutputTokens; } catch (error) { this.warn(`Error parsing session file ${sessionFilePath}: ${error}`); return 0; @@ -1170,7 +1186,7 @@ class CopilotTokenTracker implements vscode.Disposable { Token counts are estimated based on character count. CO₂, tree equivalents, water usage, and costs are derived from these token estimates.

    -
  • Cost Estimate: Based on OpenAI API pricing (as of Dec 2025, openai.com/api/pricing) and standard Anthropic rates. Assumes 50/50 split between input and output tokens. Note: GitHub Copilot pricing may differ from direct API usage. These are reference estimates only.
  • +
  • Cost Estimate: Based on public API pricing (see modelPricing.json for sources and rates). Uses actual input/output token counts for accurate cost calculation. Note: GitHub Copilot pricing may differ from direct API usage. These are reference estimates only.
  • CO₂ Estimate: Based on ~${this.co2Per1kTokens}g of CO₂e per 1,000 tokens.
  • Tree Equivalent: Represents the fraction of a single mature tree's annual CO₂ absorption (~${(this.co2AbsorptionPerTreePerYear / 1000).toFixed(1)} kg/year).
  • Water Estimate: Based on ~${this.waterUsagePer1kTokens}L of water per 1,000 tokens for data center cooling and operations.
  • @@ -1226,8 +1242,18 @@ class CopilotTokenTracker implements vscode.Disposable { const modelRows = Array.from(allModels).map(model => { const ratio = this.tokenEstimators[model] || 0.25; const charsPerToken = (1 / ratio).toFixed(1); - const monthlyTokens = stats.month.modelUsage[model] || 0; - const projectedTokens = calculateProjection(monthlyTokens); + + const todayUsage = stats.today.modelUsage[model] || { inputTokens: 0, outputTokens: 0 }; + const monthUsage = stats.month.modelUsage[model] || { inputTokens: 0, outputTokens: 0 }; + + const todayTotal = todayUsage.inputTokens + todayUsage.outputTokens; + const monthTotal = monthUsage.inputTokens + monthUsage.outputTokens; + const projectedTokens = calculateProjection(monthTotal); + + const todayInputPercent = todayTotal > 0 ? ((todayUsage.inputTokens / todayTotal) * 100).toFixed(0) : 0; + const todayOutputPercent = todayTotal > 0 ? ((todayUsage.outputTokens / todayTotal) * 100).toFixed(0) : 0; + const monthInputPercent = monthTotal > 0 ? ((monthUsage.inputTokens / monthTotal) * 100).toFixed(0) : 0; + const monthOutputPercent = monthTotal > 0 ? ((monthUsage.outputTokens / monthTotal) * 100).toFixed(0) : 0; return ` @@ -1235,8 +1261,14 @@ class CopilotTokenTracker implements vscode.Disposable { ${this.getModelDisplayName(model)} (~${charsPerToken} chars/tk) - ${(stats.today.modelUsage[model] || 0).toLocaleString()} - ${monthlyTokens.toLocaleString()} + + ${todayTotal.toLocaleString()} +
    ↑${todayInputPercent}% ↓${todayOutputPercent}%
    + + + ${monthTotal.toLocaleString()} +
    ↑${monthInputPercent}% ↓${monthOutputPercent}%
    + ${Math.round(projectedTokens).toLocaleString()} `;