Last active
March 18, 2026 09:16
-
-
Save kibotu/72c457c48e18e6fedb69d8699d935148 to your computer and use it in GitHub Desktop.
Rough estimate for token usage by cursor CSV export.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| error_reporting(0); | |
| $csv = __DIR__ . '/all-time.csv'; | |
| /** | |
| * Cursor CSV Cost Calculator | |
| * Computes estimated API cost based on model and token usage. | |
| * | |
| * Pricing (per 1M tokens, USD) β verified March 2026 | |
| * Sources: Anthropic docs, OpenAI pricing page, Google AI dev / Vertex docs, | |
| * OpenRouter, Moonshot AI platform | |
| * | |
| * Format: 'model-key' => [input, cache_write, cache_read, output] | |
| * All values are cost per 1,000,000 tokens in USD. | |
| * cache_write / cache_read = null if not supported / not documented by provider. | |
| * | |
| * NOTE ON SUBSCRIPTION-ONLY MODELS (no public API token pricing): | |
| * auto, composer-1, composer-1.5, agent_review | |
| * β Cursor-internal routing/agent layer, not direct API calls. | |
| * gpt-5.3-codex-spark-preview, gpt-5.3-codex-spark-preview-high, | |
| * gpt-5.3-codex-spark-preview-xhigh | |
| * β Research preview on Cerebras hardware; no public API pricing as of Mar 2026 | |
| * (available only to ChatGPT Pro subscribers and select design partners). | |
| * These will appear in the "No pricing data" section of the summary. | |
| */ | |
| const PRICING = [ | |
| // ββ Anthropic ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Haiku 4.5 β $1/$5 per 1M (Anthropic docs, OpenRouter Mar 2026) | |
| 'claude-4.5-haiku' => ['input' => 1.00, 'cache_write' => 1.25, 'cache_read' => 0.10, 'output' => 5.00], | |
| // Sonnet 4.5 / 4.6 β $3/$15 per 1M | |
| 'claude-4.5-sonnet' => ['input' => 3.00, 'cache_write' => 3.75, 'cache_read' => 0.30, 'output' => 15.00], | |
| 'claude-4.5-sonnet-thinking' => ['input' => 3.00, 'cache_write' => 3.75, 'cache_read' => 0.30, 'output' => 15.00], | |
| 'claude-4.6-sonnet' => ['input' => 3.00, 'cache_write' => 3.75, 'cache_read' => 0.30, 'output' => 15.00], | |
| // Cursor-internal thinking variant of Sonnet 4.6, billed at same rate | |
| 'claude-4.6-sonnet-medium-thinking' => ['input' => 3.00, 'cache_write' => 3.75, 'cache_read' => 0.30, 'output' => 15.00], | |
| // Opus 4.5 / 4.6 β $5/$25 per 1M | |
| 'claude-4.5-opus' => ['input' => 5.00, 'cache_write' => 6.25, 'cache_read' => 0.50, 'output' => 25.00], | |
| 'claude-4.5-opus-high' => ['input' => 5.00, 'cache_write' => 6.25, 'cache_read' => 0.50, 'output' => 25.00], | |
| 'claude-4.5-opus-high-thinking' => ['input' => 5.00, 'cache_write' => 6.25, 'cache_read' => 0.50, 'output' => 25.00], | |
| 'claude-4.6-opus' => ['input' => 5.00, 'cache_write' => 6.25, 'cache_read' => 0.50, 'output' => 25.00], | |
| 'claude-4.6-opus-high' => ['input' => 5.00, 'cache_write' => 6.25, 'cache_read' => 0.50, 'output' => 25.00], | |
| 'claude-4.6-opus-high-thinking' => ['input' => 5.00, 'cache_write' => 6.25, 'cache_read' => 0.50, 'output' => 25.00], | |
| // Legacy | |
| 'claude-4-sonnet' => ['input' => 3.00, 'cache_write' => 3.75, 'cache_read' => 0.30, 'output' => 15.00], | |
| 'claude-3.5-sonnet' => ['input' => 3.00, 'cache_write' => 3.75, 'cache_read' => 0.30, 'output' => 15.00], | |
| // ββ OpenAI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // gpt-5 β $1.25/$10 (OpenAI pricing page) | |
| 'gpt-5' => ['input' => 1.25, 'cache_write' => null, 'cache_read' => 0.625, 'output' => 10.00], | |
| // gpt-5.2 β $1.75/$14 (OpenAI pricing page) | |
| 'gpt-5.2' => ['input' => 1.75, 'cache_write' => null, 'cache_read' => 0.175, 'output' => 14.00], | |
| // gpt-5.3-codex β $1.75/$14 (OpenRouter, pricepertoken.com Feb/Mar 2026) | |
| 'gpt-5.3-codex' => ['input' => 1.75, 'cache_write' => null, 'cache_read' => 0.175, 'output' => 14.00], | |
| // gpt-4o β $2.50/$10 | |
| 'gpt-4o' => ['input' => 2.50, 'cache_write' => null, 'cache_read' => 1.25, 'output' => 10.00], | |
| // ββ Google Gemini ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Gemini 3 Flash Preview β $0.50/$3 (Google blog Jan 2026, OpenRouter) | |
| 'gemini-3-flash-preview' => ['input' => 0.50, 'cache_write' => null, 'cache_read' => 0.05, 'output' => 3.00], | |
| // Gemini 3 Pro Preview β $2/$12 (OpenRouter, MetaCTO Jan 2026) | |
| 'gemini-3-pro-preview' => ['input' => 2.00, 'cache_write' => null, 'cache_read' => 0.20, 'output' => 12.00], | |
| 'gemini-3.1-pro-preview' => ['input' => 2.00, 'cache_write' => null, 'cache_read' => 0.20, 'output' => 12.00], | |
| // Gemini 2.5 Pro β $1.25/$10 | |
| 'gemini-2.5-pro' => ['input' => 1.25, 'cache_write' => null, 'cache_read' => 0.3125,'output' => 10.00], | |
| // ββ Moonshot AI ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Kimi K2.5 β $0.60 input / $3.00 output, cached $0.10 (VentureBeat Jan 2026, Moonshot platform) | |
| 'kimi-k2.5' => ['input' => 0.60, 'cache_write' => null, 'cache_read' => 0.10, 'output' => 3.00], | |
| ]; | |
| /** | |
| * Map Cursor model strings -> pricing key. | |
| * Cursor sometimes uses non-standard model identifiers. | |
| * Keys are ordered longest-first so the most specific prefix wins. | |
| */ | |
| function resolveModel(string $raw): ?string | |
| { | |
| $m = strtolower(trim($raw)); | |
| // Direct match first | |
| if (isset(PRICING[$m])) { | |
| return $m; | |
| } | |
| // Prefix matching β longest (most specific) entries listed first | |
| $map = [ | |
| // Anthropic | |
| 'claude-4.6-sonnet-medium-thinking' => 'claude-4.6-sonnet-medium-thinking', | |
| 'claude-4.6-opus-high-thinking' => 'claude-4.6-opus-high-thinking', | |
| 'claude-4.6-opus-high' => 'claude-4.6-opus-high', | |
| 'claude-4.6-opus' => 'claude-4.6-opus', | |
| 'claude-4.6-sonnet' => 'claude-4.6-sonnet', | |
| 'claude-4.5-opus-high-thinking' => 'claude-4.5-opus-high-thinking', | |
| 'claude-4.5-opus-high' => 'claude-4.5-opus-high', | |
| 'claude-4.5-opus' => 'claude-4.5-opus', | |
| 'claude-4.5-sonnet-thinking' => 'claude-4.5-sonnet-thinking', | |
| 'claude-4.5-sonnet' => 'claude-4.5-sonnet', | |
| 'claude-4.5-haiku' => 'claude-4.5-haiku', | |
| 'claude-4-sonnet' => 'claude-4-sonnet', | |
| 'claude-3.5-sonnet' => 'claude-3.5-sonnet', | |
| // OpenAI | |
| 'gpt-5.3-codex-spark' => null, // subscription/preview only β no public API price | |
| 'gpt-5.3-codex' => 'gpt-5.3-codex', | |
| 'gpt-5.2' => 'gpt-5.2', | |
| 'gpt-5' => 'gpt-5', | |
| 'gpt-4o' => 'gpt-4o', | |
| 'gemini-3.1-pro-preview' => 'gemini-3.1-pro-preview', | |
| 'gemini-3-flash-preview' => 'gemini-3-flash-preview', | |
| 'gemini-3-pro-preview' => 'gemini-3-pro-preview', | |
| 'gemini-2.5-pro' => 'gemini-2.5-pro', | |
| // Moonshot | |
| 'kimi-k2.5' => 'kimi-k2.5', | |
| ]; | |
| foreach ($map as $prefix => $key) { | |
| if (str_starts_with($m, $prefix)) { | |
| return $key; // null means "known but no public pricing" | |
| } | |
| } | |
| return null; // completely unknown | |
| } | |
| /** | |
| * Calculate cost in USD for a single row. | |
| * | |
| * CSV columns (0-indexed): | |
| * 0 Date | |
| * 1 User | |
| * 2 Kind | |
| * 3 Model | |
| * 4 Max Mode | |
| * 5 Input (w/ Cache Write) | |
| * 6 Input (w/o Cache Write) | |
| * 7 Cache Read | |
| * 8 Output Tokens | |
| * 9 Total Tokens | |
| * 10 Cost | |
| */ | |
| function computeRowCost(array $row): array | |
| { | |
| $rawModel = $row[3] ?? ''; | |
| $pricingKey = resolveModel($rawModel); | |
| // If Cost column already has a numeric value, use it directly | |
| $csvCost = $row[10] ?? 'Free'; | |
| if (is_numeric($csvCost)) { | |
| return [ | |
| 'model' => $rawModel, | |
| 'pricing_key' => $pricingKey ?? $rawModel, | |
| 'cost' => (float) $csvCost, | |
| 'source' => 'csv', | |
| ]; | |
| } | |
| // "Free" or unknown cost β compute from tokens if we have pricing | |
| if ($pricingKey === null) { | |
| return [ | |
| 'model' => $rawModel, | |
| 'pricing_key' => null, | |
| 'cost' => 0.0, | |
| 'source' => 'unknown-model', | |
| ]; | |
| } | |
| $p = PRICING[$pricingKey]; | |
| $inputWithCache = (int) ($row[5] ?? 0); | |
| $inputNoCache = (int) ($row[6] ?? 0); | |
| $cacheRead = (int) ($row[7] ?? 0); | |
| $output = (int) ($row[8] ?? 0); | |
| // Cache-write tokens = inputWithCache - inputNoCache | |
| $cacheWrite = max(0, $inputWithCache - $inputNoCache); | |
| $cost = ($inputNoCache / 1_000_000) * $p['input']; | |
| $cost += ($output / 1_000_000) * $p['output']; | |
| $cost += ($cacheRead / 1_000_000) * $p['cache_read']; | |
| if ($cacheWrite > 0 && $p['cache_write'] !== null) { | |
| $cost += ($cacheWrite / 1_000_000) * $p['cache_write']; | |
| } elseif ($cacheWrite > 0) { | |
| // Fallback: treat cache-write tokens as normal input | |
| $cost += ($cacheWrite / 1_000_000) * $p['input']; | |
| } | |
| return [ | |
| 'model' => $rawModel, | |
| 'pricing_key' => $pricingKey, | |
| 'cost' => $cost, | |
| 'source' => 'computed', | |
| ]; | |
| } | |
| // ββ Subscription config βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Subscription started 2025-01-01, capped at β¬20/month, running to today. | |
| * monthsSpanned is computed below from SUB_START to now (not from CSV dates). | |
| * Adjust EUR_TO_USD to the current exchange rate as needed. | |
| */ | |
| const SUBSCRIPTION_EUR = 20.0; | |
| const EUR_TO_USD = 1.08; // ~March 2026 rate; update as needed | |
| const SUB_START = '2025-01-01'; // subscription start date | |
| // ββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| $handle = fopen($csv, 'r'); | |
| if ($handle === false) { | |
| throw new RuntimeException("Failed to open CSV file: $csv"); | |
| } | |
| $header = fgetcsv($handle); | |
| if ($header === false) { | |
| throw new RuntimeException('Failed to read CSV header'); | |
| } | |
| // Parse dates to figure out how many calendar months the CSV spans | |
| $firstDate = null; | |
| $lastDate = null; | |
| $totalCost = 0.0; | |
| $totalTokens = 0; | |
| $rowCount = 0; | |
| $unknownModels = []; | |
| $perModel = []; // ['pricing_key' => ['cost' => float, 'tokens' => int]] | |
| while (($row = fgetcsv($handle)) !== false) { | |
| if (count($row) < 9) { | |
| continue; // skip malformed rows | |
| } | |
| $result = computeRowCost($row); | |
| $tokens = (int) ($row[9] ?? 0); // Total Tokens column | |
| $totalCost += $result['cost']; | |
| $totalTokens += $tokens; | |
| $rowCount++; | |
| // Track date range (column 0) | |
| $dateStr = trim($row[0] ?? ''); | |
| if ($dateStr !== '') { | |
| $ts = strtotime($dateStr); | |
| if ($ts !== false) { | |
| if ($firstDate === null || $ts < $firstDate) { | |
| $firstDate = $ts; | |
| } | |
| if ($lastDate === null || $ts > $lastDate) { | |
| $lastDate = $ts; | |
| } | |
| } | |
| } | |
| $mk = $result['pricing_key'] ?? ('UNKNOWN:' . $result['model']); | |
| $perModel[$mk]['cost'] = ($perModel[$mk]['cost'] ?? 0.0) + $result['cost']; | |
| $perModel[$mk]['tokens'] = ($perModel[$mk]['tokens'] ?? 0) + $tokens; | |
| if ($result['source'] === 'unknown-model') { | |
| $unknownModels[$result['model']] = ($unknownModels[$result['model']] ?? 0) + 1; | |
| } | |
| } | |
| fclose($handle); | |
| // ββ Derive subscription months: 2025-01-01 β today βββββββββββββββββββββββββββ | |
| $subStart = new DateTime(SUB_START); | |
| $today = new DateTime('today'); | |
| // Whole calendar months elapsed since subscription start (inclusive of start month) | |
| $subY1 = (int) $subStart->format('Y'); | |
| $subM1 = (int) $subStart->format('n'); | |
| $subY2 = (int) $today->format('Y'); | |
| $subM2 = (int) $today->format('n'); | |
| $monthsSpanned = max(1, ($subY2 - $subY1) * 12 + ($subM2 - $subM1) + 1); | |
| $totalSubscriptionCost = SUBSCRIPTION_EUR * EUR_TO_USD * $monthsSpanned; | |
| // ββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| echo PHP_EOL; | |
| echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ\n"; | |
| echo "β Cursor Usage Cost Summary β\n"; | |
| echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ\n\n"; | |
| $avgPerToken = fn(float $cost, int $tokens): string | |
| => $tokens > 0 ? '$' . number_format($cost / $tokens, 8) : '-'; | |
| echo sprintf(" %-40s %14s %16s %14s\n", 'Model (pricing key)', 'Total Tokens', 'Avg $/token', 'Cost (USD)'); | |
| echo " " . str_repeat('β', 90) . "\n"; | |
| uasort($perModel, fn($a, $b) => $b['cost'] <=> $a['cost']); | |
| foreach ($perModel as $model => $data) { | |
| echo sprintf( | |
| " %-40s %14s %16s %14s\n", | |
| $model, | |
| number_format($data['tokens']), | |
| $avgPerToken($data['cost'], $data['tokens']), | |
| '$' . number_format($data['cost'], 4) | |
| ); | |
| } | |
| if (!empty($unknownModels)) { | |
| echo PHP_EOL; | |
| echo " ββ No pricing data (counted as \$0) ββββββββββββββββββββββββββββββββββββββββββββββββ\n"; | |
| arsort($unknownModels); | |
| foreach ($unknownModels as $model => $count) { | |
| echo sprintf(" %-40s %14s %16s %14s\n", $model, "($count rows)", '-', '-'); | |
| } | |
| } | |
| echo " " . str_repeat('β', 90) . "\n"; | |
| echo sprintf( | |
| " %-40s %14s %16s %14s\n", | |
| "TOTAL ($rowCount rows)", | |
| number_format($totalTokens), | |
| $avgPerToken($totalCost, $totalTokens), | |
| '$' . number_format($totalCost, 4) | |
| ); | |
| echo PHP_EOL; | |
| // ββ Subscription comparison βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Effective cost per 1M tokens under each scenario | |
| $apiCostPer1M = $totalTokens > 0 ? ($totalCost / $totalTokens) * 1_000_000 : 0.0; | |
| $subCostPer1M = $totalTokens > 0 ? ($totalSubscriptionCost / $totalTokens) * 1_000_000 : 0.0; | |
| $savingsUsd = $totalSubscriptionCost - $totalCost; | |
| $savingsPct = $totalSubscriptionCost > 0 ? ($savingsUsd / $totalSubscriptionCost) * 100 : 0.0; | |
| // How many tokens/month would you need to consume for the sub to break even vs API | |
| $avgApiCostPerToken = $totalTokens > 0 ? $totalCost / $totalTokens : 0.0; | |
| $subCostPerMonth = SUBSCRIPTION_EUR * EUR_TO_USD; | |
| $breakevenPerMonth = $avgApiCostPerToken > 0 ? (int) round($subCostPerMonth / $avgApiCostPerToken) : 0; | |
| $actualPerMonth = $monthsSpanned > 0 ? (int) round($totalTokens / $monthsSpanned) : 0; | |
| echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ\n"; | |
| echo "β Subscription Value Analysis β\n"; | |
| echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ\n\n"; | |
| echo sprintf(" Subscription: β¬%.2f/mo Γ %d months (%s β %s) = \$%.4f (@ %.4f USD/EUR)\n\n", | |
| SUBSCRIPTION_EUR, | |
| $monthsSpanned, | |
| SUB_START, | |
| $today->format('Y-m-d'), | |
| $totalSubscriptionCost, | |
| EUR_TO_USD | |
| ); | |
| $w = 38; | |
| echo " " . str_repeat('β', 70) . "\n"; | |
| echo sprintf(" %-{$w}s %14s %14s\n", '', 'API (pay-per-use)', 'Subscription'); | |
| echo " " . str_repeat('β', 70) . "\n"; | |
| echo sprintf(" %-{$w}s %14s %14s\n", 'Total cost (USD)', | |
| '$' . number_format($totalCost, 4), | |
| '$' . number_format($totalSubscriptionCost, 4) | |
| ); | |
| echo sprintf(" %-{$w}s %14s %14s\n", 'Effective $/1M tokens', | |
| '$' . number_format($apiCostPer1M, 4), | |
| '$' . number_format($subCostPer1M, 4) | |
| ); | |
| echo sprintf(" %-{$w}s %14s %14s\n", 'Effective $/token', | |
| '$' . number_format($avgApiCostPerToken, 8), | |
| '$' . number_format($totalTokens > 0 ? $totalSubscriptionCost / $totalTokens : 0, 8) | |
| ); | |
| echo " " . str_repeat('β', 70) . "\n\n"; | |
| if ($savingsUsd >= 0) { | |
| echo sprintf(" You SAVED \$%s vs pay-per-use (%.1f%% of sub cost β sub was the pricier option)\n", | |
| number_format($savingsUsd, 4), $savingsPct); | |
| } else { | |
| echo sprintf(" You SAVED \$%s vs pay-per-use (sub was %.1f%% cheaper than API billing)\n", | |
| number_format(abs($savingsUsd), 4), abs($savingsPct)); | |
| } | |
| echo PHP_EOL; | |
| echo sprintf(" Avg tokens/month (actual): %s\n", number_format($actualPerMonth)); | |
| echo sprintf(" Breakeven tokens/month: %s β above this the sub wins, below API wins\n", | |
| $breakevenPerMonth > 0 ? number_format($breakevenPerMonth) : 'n/a'); | |
| echo PHP_EOL; | |
| echo " Pricing source: Anthropic / OpenAI / Google / Moonshot docs, March 2026\n"; | |
| echo " EURβUSD rate used: " . EUR_TO_USD . " (update EUR_TO_USD constant if needed)\n"; | |
| echo " Note: 'Free'/'Included' rows are estimated from token counts\n"; | |
| echo " using official API rates.\n"; | |
| echo " Note: gpt-5.3-codex-spark-* variants have no public API pricing\n"; | |
| echo " (subscription/research preview only as of March 2026).\n\n"; |
Author
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
my usage up until today if it were prices from today