claude-code-wrapped 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.python-version +1 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/cli.js +62 -0
- package/claude_code_wrapped/__init__.py +3 -0
- package/claude_code_wrapped/main.py +102 -0
- package/claude_code_wrapped/pricing.py +179 -0
- package/claude_code_wrapped/reader.py +267 -0
- package/claude_code_wrapped/stats.py +339 -0
- package/claude_code_wrapped/ui.py +604 -0
- package/package.json +33 -0
- package/pyproject.toml +35 -0
- package/uv.lock +57 -0
package/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mert Deveci
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Claude Code Wrapped
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
░█████╗░██╗░░░░░░█████╗░██╗░░░██╗██████╗░███████╗
|
|
5
|
+
██╔══██╗██║░░░░░██╔══██╗██║░░░██║██╔══██╗██╔════╝
|
|
6
|
+
██║░░╚═╝██║░░░░░███████║██║░░░██║██║░░██║█████╗░░
|
|
7
|
+
██║░░██╗██║░░░░░██╔══██║██║░░░██║██║░░██║██╔══╝░░
|
|
8
|
+
╚█████╔╝███████╗██║░░██║╚██████╔╝██████╔╝███████╗
|
|
9
|
+
░╚════╝░╚══════╝╚═╝░░╚═╝░╚═════╝░╚═════╝░╚══════╝
|
|
10
|
+
|
|
11
|
+
C O D E W R A P P E D 2025
|
|
12
|
+
by Banker.so
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Your year with Claude Code, Spotify Wrapped style.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Using uvx (recommended)
|
|
21
|
+
uvx claude-code-wrapped
|
|
22
|
+
|
|
23
|
+
# Using npx
|
|
24
|
+
npx claude-code-wrapped
|
|
25
|
+
|
|
26
|
+
# Or install via pip
|
|
27
|
+
pip install claude-code-wrapped
|
|
28
|
+
claude-code-wrapped
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Press `Enter` to advance through your personalized stats.
|
|
32
|
+
|
|
33
|
+
## What You'll See
|
|
34
|
+
|
|
35
|
+
**Dramatic stat reveals** - one at a time, like Spotify Wrapped:
|
|
36
|
+
- Total messages exchanged
|
|
37
|
+
- Coding sessions
|
|
38
|
+
- Tokens processed
|
|
39
|
+
- Your longest streak
|
|
40
|
+
|
|
41
|
+
**GitHub-style contribution graph** - see your coding patterns at a glance
|
|
42
|
+
|
|
43
|
+
**Your coding personality** - based on your habits:
|
|
44
|
+
- 🦉 Night Owl
|
|
45
|
+
- 🔥 Streak Master
|
|
46
|
+
- ⚡ Terminal Warrior
|
|
47
|
+
- 🎨 The Refactorer
|
|
48
|
+
- 🚀 Empire Builder
|
|
49
|
+
- 🌙 Weekend Warrior
|
|
50
|
+
- 🎯 Perfectionist
|
|
51
|
+
- 💻 Dedicated Dev
|
|
52
|
+
|
|
53
|
+
**Fun facts & bloopers** - like "You coded after midnight 47 times"
|
|
54
|
+
|
|
55
|
+
**Movie-style credits** - featuring your top tools, projects, and models
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
claude-code-wrapped # Full cinematic experience
|
|
61
|
+
claude-code-wrapped --no-animate # Skip to dashboard view
|
|
62
|
+
claude-code-wrapped --json # Export stats as JSON
|
|
63
|
+
claude-code-wrapped 2025 # View a specific year
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Python 3.12+ (with uvx, pipx, or pip)
|
|
69
|
+
- Or Node.js 16+ (for npx)
|
|
70
|
+
- Claude Code installed (`~/.claude/` directory exists)
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
Reads your local Claude Code conversation history from `~/.claude/projects/` and aggregates:
|
|
75
|
+
- Message counts and timestamps
|
|
76
|
+
- Token usage (input, output, cache)
|
|
77
|
+
- Tool usage (Bash, Read, Edit, etc.)
|
|
78
|
+
- Model preferences (Opus, Sonnet, Haiku)
|
|
79
|
+
- Project activity
|
|
80
|
+
|
|
81
|
+
All data stays local. Nothing is sent anywhere.
|
|
82
|
+
|
|
83
|
+
## Privacy
|
|
84
|
+
|
|
85
|
+
This tool is completely local and privacy-focused:
|
|
86
|
+
|
|
87
|
+
- **No network requests** - All data is read from your local `~/.claude/` directory
|
|
88
|
+
- **No data collection** - Nothing is sent to any server
|
|
89
|
+
- **No API keys needed** - Works entirely offline
|
|
90
|
+
- **No secrets exposed** - Only aggregated stats are shown, not conversation content
|
|
91
|
+
|
|
92
|
+
## Author
|
|
93
|
+
|
|
94
|
+
Built by [Mert Deveci](https://x.com/gm_mertd), Maker of [Banker.so](https://banker.so)
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT - see [LICENSE](LICENSE) for details.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Pass through all arguments
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
// Check if a command exists
|
|
9
|
+
function commandExists(cmd) {
|
|
10
|
+
const result = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
|
|
11
|
+
return result.status === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Check if Python module is installed
|
|
15
|
+
function pythonModuleExists() {
|
|
16
|
+
const result = spawnSync('python3', ['-c', 'import claude_code_wrapped'], { stdio: 'ignore' });
|
|
17
|
+
return result.status === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Try to find the best way to run the Python package
|
|
21
|
+
function findExecutor() {
|
|
22
|
+
// Try uvx first (recommended)
|
|
23
|
+
if (commandExists('uvx')) {
|
|
24
|
+
return { cmd: 'uvx', args: ['claude-code-wrapped', ...args] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try pipx
|
|
28
|
+
if (commandExists('pipx')) {
|
|
29
|
+
return { cmd: 'pipx', args: ['run', 'claude-code-wrapped', ...args] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try direct python module
|
|
33
|
+
if (pythonModuleExists()) {
|
|
34
|
+
return { cmd: 'python3', args: ['-m', 'claude_code_wrapped', ...args] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// No executor found
|
|
38
|
+
console.error('claude-code-wrapped requires Python 3.12+ and one of: uvx, pipx, or pip');
|
|
39
|
+
console.error('');
|
|
40
|
+
console.error('Recommended: Install uv (https://docs.astral.sh/uv/) then run:');
|
|
41
|
+
console.error(' uvx claude-code-wrapped');
|
|
42
|
+
console.error('');
|
|
43
|
+
console.error('Or install with pip:');
|
|
44
|
+
console.error(' pip install claude-code-wrapped');
|
|
45
|
+
console.error(' claude-code-wrapped');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const executor = findExecutor();
|
|
50
|
+
const child = spawn(executor.cmd, executor.args, {
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
env: process.env
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
child.on('close', (code) => {
|
|
56
|
+
process.exit(code || 0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
child.on('error', (err) => {
|
|
60
|
+
console.error('Failed to start:', err.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Claude Code Wrapped - Main entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from .reader import get_claude_dir, load_all_messages
|
|
10
|
+
from .stats import aggregate_stats
|
|
11
|
+
from .ui import render_wrapped
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
"""Main entry point for Claude Code Wrapped."""
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Claude Code Wrapped - Your year with Claude Code",
|
|
18
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
19
|
+
epilog="""
|
|
20
|
+
Examples:
|
|
21
|
+
claude-code-wrapped Show your wrapped for current year
|
|
22
|
+
claude-code-wrapped 2025 Show your 2025 wrapped
|
|
23
|
+
claude-code-wrapped --no-animate Skip animations
|
|
24
|
+
""",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"year",
|
|
28
|
+
type=int,
|
|
29
|
+
nargs="?",
|
|
30
|
+
default=datetime.now().year,
|
|
31
|
+
help="Year to analyze (default: current year)",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--no-animate",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Disable animations for faster display",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--json",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Output raw stats as JSON",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
console = Console()
|
|
46
|
+
|
|
47
|
+
# Check for Claude directory
|
|
48
|
+
try:
|
|
49
|
+
claude_dir = get_claude_dir()
|
|
50
|
+
except FileNotFoundError as e:
|
|
51
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
52
|
+
console.print("\nMake sure you have Claude Code installed and have used it at least once.")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
# Load messages
|
|
56
|
+
if not args.json:
|
|
57
|
+
console.print(f"\n[dim]Loading your Claude Code history for {args.year}...[/dim]\n")
|
|
58
|
+
|
|
59
|
+
messages = load_all_messages(claude_dir, year=args.year)
|
|
60
|
+
|
|
61
|
+
if not messages:
|
|
62
|
+
console.print(f"[yellow]No Claude Code activity found for {args.year}.[/yellow]")
|
|
63
|
+
console.print("\nTry a different year or make sure you've used Claude Code.")
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
# Calculate stats
|
|
67
|
+
stats = aggregate_stats(messages, args.year)
|
|
68
|
+
|
|
69
|
+
# Output
|
|
70
|
+
if args.json:
|
|
71
|
+
import json
|
|
72
|
+
output = {
|
|
73
|
+
"year": stats.year,
|
|
74
|
+
"total_messages": stats.total_messages,
|
|
75
|
+
"total_user_messages": stats.total_user_messages,
|
|
76
|
+
"total_assistant_messages": stats.total_assistant_messages,
|
|
77
|
+
"total_sessions": stats.total_sessions,
|
|
78
|
+
"total_projects": stats.total_projects,
|
|
79
|
+
"total_tokens": stats.total_tokens,
|
|
80
|
+
"total_input_tokens": stats.total_input_tokens,
|
|
81
|
+
"total_output_tokens": stats.total_output_tokens,
|
|
82
|
+
"active_days": stats.active_days,
|
|
83
|
+
"streak_longest": stats.streak_longest,
|
|
84
|
+
"streak_current": stats.streak_current,
|
|
85
|
+
"most_active_hour": stats.most_active_hour,
|
|
86
|
+
"most_active_day": stats.most_active_day[0].isoformat() if stats.most_active_day else None,
|
|
87
|
+
"most_active_day_messages": stats.most_active_day[1] if stats.most_active_day else None,
|
|
88
|
+
"primary_model": stats.primary_model,
|
|
89
|
+
"top_tools": dict(stats.top_tools),
|
|
90
|
+
"top_projects": dict(stats.top_projects),
|
|
91
|
+
"hourly_distribution": stats.hourly_distribution,
|
|
92
|
+
"weekday_distribution": stats.weekday_distribution,
|
|
93
|
+
"estimated_cost_usd": stats.estimated_cost,
|
|
94
|
+
"cost_by_model": stats.cost_by_model,
|
|
95
|
+
}
|
|
96
|
+
print(json.dumps(output, indent=2))
|
|
97
|
+
else:
|
|
98
|
+
render_wrapped(stats, console, animate=not args.no_animate)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Pricing data and cost calculation for Claude models.
|
|
2
|
+
|
|
3
|
+
Based on official Anthropic pricing as of December 2025.
|
|
4
|
+
Source: https://docs.anthropic.com/en/docs/about-claude/models
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ModelPricing:
|
|
12
|
+
"""Pricing for a Claude model in USD per million tokens."""
|
|
13
|
+
input_cost: float # Base input tokens
|
|
14
|
+
output_cost: float # Output tokens
|
|
15
|
+
cache_write_cost: float # 5-minute cache writes (1.25x input)
|
|
16
|
+
cache_read_cost: float # Cache hits & refreshes (0.1x input)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Official Anthropic pricing as of December 2025 (USD per million tokens)
|
|
20
|
+
# Source: https://platform.claude.com/docs/en/about-claude/pricing
|
|
21
|
+
MODEL_PRICING: dict[str, ModelPricing] = {
|
|
22
|
+
# Claude 4.x models
|
|
23
|
+
"claude-opus-4-5-20251101": ModelPricing(5.0, 25.0, 6.25, 0.50),
|
|
24
|
+
"claude-opus-4-20250514": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
25
|
+
"claude-opus-4-1-20250620": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
26
|
+
"claude-sonnet-4-5-20250514": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
27
|
+
"claude-sonnet-4-20250514": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
28
|
+
"claude-haiku-4-5-20251101": ModelPricing(1.0, 5.0, 1.25, 0.10),
|
|
29
|
+
|
|
30
|
+
# Claude 3.x models
|
|
31
|
+
"claude-3-5-sonnet-20241022": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
32
|
+
"claude-3-5-sonnet-20240620": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
33
|
+
"claude-3-5-haiku-20241022": ModelPricing(0.80, 4.0, 1.0, 0.08),
|
|
34
|
+
"claude-3-opus-20240229": ModelPricing(15.0, 75.0, 18.75, 1.50),
|
|
35
|
+
"claude-3-sonnet-20240229": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
36
|
+
"claude-3-haiku-20240307": ModelPricing(0.25, 1.25, 0.30, 0.03),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Simplified model name mappings for display
|
|
40
|
+
MODEL_FAMILY_PRICING: dict[str, ModelPricing] = {
|
|
41
|
+
"opus": ModelPricing(15.0, 75.0, 18.75, 1.50), # Conservative estimate
|
|
42
|
+
"sonnet": ModelPricing(3.0, 15.0, 3.75, 0.30),
|
|
43
|
+
"haiku": ModelPricing(0.80, 4.0, 1.0, 0.08), # Use 3.5 Haiku as default
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_model_pricing(model_name: str | None) -> ModelPricing | None:
|
|
48
|
+
"""Get pricing for a model by name.
|
|
49
|
+
|
|
50
|
+
Handles both full model IDs and simplified names like 'Opus', 'Sonnet', 'Haiku'.
|
|
51
|
+
"""
|
|
52
|
+
if not model_name:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
model_lower = model_name.lower()
|
|
56
|
+
|
|
57
|
+
# Try exact match first
|
|
58
|
+
if model_lower in MODEL_PRICING:
|
|
59
|
+
return MODEL_PRICING[model_lower]
|
|
60
|
+
|
|
61
|
+
# For simple names like "Opus", "Sonnet", "Haiku", use family pricing
|
|
62
|
+
# This is what we get from the stats module's simplified model names
|
|
63
|
+
for family, pricing in MODEL_FAMILY_PRICING.items():
|
|
64
|
+
if model_lower == family:
|
|
65
|
+
return pricing
|
|
66
|
+
|
|
67
|
+
# Try partial match on full model IDs (for full model names like "claude-3-5-sonnet-...")
|
|
68
|
+
for model_id, pricing in MODEL_PRICING.items():
|
|
69
|
+
if model_lower in model_id or model_id in model_lower:
|
|
70
|
+
return pricing
|
|
71
|
+
|
|
72
|
+
# Fall back to family-based pricing for partial matches
|
|
73
|
+
for family, pricing in MODEL_FAMILY_PRICING.items():
|
|
74
|
+
if family in model_lower:
|
|
75
|
+
return pricing
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calculate_cost(
|
|
81
|
+
input_tokens: int,
|
|
82
|
+
output_tokens: int,
|
|
83
|
+
cache_creation_tokens: int = 0,
|
|
84
|
+
cache_read_tokens: int = 0,
|
|
85
|
+
model_name: str | None = None,
|
|
86
|
+
) -> float | None:
|
|
87
|
+
"""Calculate cost in USD for a given token usage.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
input_tokens: Number of input tokens (excluding cache)
|
|
91
|
+
output_tokens: Number of output tokens
|
|
92
|
+
cache_creation_tokens: Number of cache write tokens
|
|
93
|
+
cache_read_tokens: Number of cache read tokens
|
|
94
|
+
model_name: Model name or ID for pricing lookup
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Cost in USD, or None if pricing unavailable
|
|
98
|
+
"""
|
|
99
|
+
pricing = get_model_pricing(model_name)
|
|
100
|
+
if not pricing:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Convert to millions and calculate
|
|
104
|
+
cost = (
|
|
105
|
+
(input_tokens / 1_000_000) * pricing.input_cost +
|
|
106
|
+
(output_tokens / 1_000_000) * pricing.output_cost +
|
|
107
|
+
(cache_creation_tokens / 1_000_000) * pricing.cache_write_cost +
|
|
108
|
+
(cache_read_tokens / 1_000_000) * pricing.cache_read_cost
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return cost
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def calculate_total_cost_by_model(
|
|
115
|
+
model_usage: dict[str, dict[str, int]]
|
|
116
|
+
) -> tuple[float, dict[str, float]]:
|
|
117
|
+
"""Calculate total cost across all models.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
model_usage: Dict mapping model names to token counts:
|
|
121
|
+
{
|
|
122
|
+
"Sonnet": {"input": 1000, "output": 500, "cache_create": 0, "cache_read": 0},
|
|
123
|
+
"Opus": {"input": 500, "output": 200, ...},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple of (total_cost, per_model_costs)
|
|
128
|
+
"""
|
|
129
|
+
total = 0.0
|
|
130
|
+
per_model: dict[str, float] = {}
|
|
131
|
+
|
|
132
|
+
for model_name, tokens in model_usage.items():
|
|
133
|
+
cost = calculate_cost(
|
|
134
|
+
input_tokens=tokens.get("input", 0),
|
|
135
|
+
output_tokens=tokens.get("output", 0),
|
|
136
|
+
cache_creation_tokens=tokens.get("cache_create", 0),
|
|
137
|
+
cache_read_tokens=tokens.get("cache_read", 0),
|
|
138
|
+
model_name=model_name,
|
|
139
|
+
)
|
|
140
|
+
if cost is not None:
|
|
141
|
+
per_model[model_name] = cost
|
|
142
|
+
total += cost
|
|
143
|
+
|
|
144
|
+
return total, per_model
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_cost(cost: float | None) -> str:
|
|
148
|
+
"""Format cost for display."""
|
|
149
|
+
if cost is None:
|
|
150
|
+
return "N/A"
|
|
151
|
+
if cost < 0.01:
|
|
152
|
+
return f"${cost:.4f}"
|
|
153
|
+
if cost < 1:
|
|
154
|
+
return f"${cost:.2f}"
|
|
155
|
+
if cost < 100:
|
|
156
|
+
return f"${cost:.2f}"
|
|
157
|
+
if cost < 1000:
|
|
158
|
+
return f"${cost:.0f}"
|
|
159
|
+
return f"${cost:,.0f}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
# Test pricing lookups
|
|
164
|
+
print("Testing pricing lookups:")
|
|
165
|
+
test_models = ["Opus", "Sonnet", "Haiku", "claude-3-5-sonnet-20241022", "unknown"]
|
|
166
|
+
for model in test_models:
|
|
167
|
+
pricing = get_model_pricing(model)
|
|
168
|
+
print(f" {model}: {pricing}")
|
|
169
|
+
|
|
170
|
+
# Test cost calculation
|
|
171
|
+
print("\nTesting cost calculation:")
|
|
172
|
+
cost = calculate_cost(
|
|
173
|
+
input_tokens=1_000_000,
|
|
174
|
+
output_tokens=500_000,
|
|
175
|
+
cache_creation_tokens=100_000,
|
|
176
|
+
cache_read_tokens=50_000,
|
|
177
|
+
model_name="Sonnet"
|
|
178
|
+
)
|
|
179
|
+
print(f" 1M input + 500K output + 100K cache create + 50K cache read (Sonnet): {format_cost(cost)}")
|