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 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.