Skip to content

Add EUR currency support to cost calculation functions #165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

TokenCost is a Python package that calculates USD costs for Large Language Model (LLM) API usage by estimating token counts and applying current pricing. It's designed for AI developers building agents and applications that need to track LLM costs across providers like OpenAI and Anthropic.

## Development Commands

### Installation and Setup
```bash
pip install -e .[dev] # Install in development mode with dev dependencies
```

### Testing
```bash
# Run tests with coverage
python -m pytest tests/
coverage run --source tokencost -m pytest
coverage report -m

# Using tox (recommended)
tox # Run tests and linting across environments
```

### Code Quality
```bash
# Linting
flake8 tokencost/
tox -e flake8

# Dependency validation
tach check # Validate module dependencies according to tach.yml
```

### Price Updates
```bash
python update_prices.py # Update model pricing data from LiteLLM
```

## Architecture

### Core Modules

- **`tokencost/costs.py`** - Main cost calculation and token counting logic
- **`tokencost/constants.py`** - Price data management and fetching utilities
- **`tokencost/model_prices.json`** - Static pricing data for all supported models

### Key APIs

The main package exports these functions from `tokencost/__init__.py`:
- `count_message_tokens()` - Count tokens in ChatML message format
- `count_string_tokens()` - Count tokens in raw strings
- `calculate_prompt_cost()` - Calculate cost for input prompts
- `calculate_completion_cost()` - Calculate cost for model completions
- `calculate_all_costs_and_tokens()` - Comprehensive cost and token analysis

### Token Counting Strategy

- **OpenAI models**: Uses tiktoken (official tokenizer)
- **Anthropic models v3+**: Uses Anthropic's beta token counting API
- **Older Anthropic models**: Approximates with tiktoken cl100k_base encoding

### Module Dependencies

Following the `tach.yml` configuration:
- `tokencost` depends on `tokencost.constants` and `tokencost.costs`
- `tokencost.costs` depends on `tokencost.constants`
- `update_prices` depends on `tokencost`

## Development Notes

### Price Data Management
- Pricing data is automatically updated daily via GitHub Actions
- Updates pull from LiteLLM's cost tracker at `https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json`
- Manual updates can be triggered with `python update_prices.py`

### Testing
- Tests are in `tests/test_costs.py`
- Focus on cost calculation accuracy and token counting precision
- Coverage reporting is enabled and should be maintained

### Code Standards
- Maximum line length: 120 characters (flake8 configuration)
- Python 3.10+ required
- Import organization follows flake8 rules with F401 exceptions in `__init__.py`

### Package Distribution
- Uses modern `pyproject.toml` configuration
- `model_prices.json` is included in distribution via `MANIFEST.in`
- Published to PyPI as `tokencost`
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Building AI agents? Check out [AgentOps](https://agentops.ai/?tokencost)

### Features
* **LLM Price Tracking** Major LLM providers frequently add new models and update pricing. This repo helps track the latest price changes
* **Multi-Currency Support** Calculate costs in USD or EUR with real-time exchange rates
* **Token counting** Accurately count prompt tokens before sending OpenAI requests
* **Easy integration** Get the cost of a prompt or completion with a single function

Expand All @@ -48,6 +49,13 @@ completion_cost = calculate_completion_cost(completion, model)

print(f"{prompt_cost} + {completion_cost} = {prompt_cost + completion_cost}")
# 0.0000135 + 0.000014 = 0.0000275

# Calculate costs in EUR
prompt_cost_eur = calculate_prompt_cost(prompt, model, currency="EUR")
completion_cost_eur = calculate_completion_cost(completion, model, currency="EUR")

print(f"EUR: {prompt_cost_eur} + {completion_cost_eur} = {prompt_cost_eur + completion_cost_eur}")
# EUR: 0.0000121 + 0.000013 = 0.0000251
```

## Installation
Expand Down Expand Up @@ -93,6 +101,11 @@ model= "gpt-3.5-turbo"
prompt_cost = calculate_prompt_cost(prompt_string, model)
print(f"Cost: ${prompt_cost}")
# Cost: $3e-06

# Calculate in EUR
prompt_cost_eur = calculate_prompt_cost(prompt_string, model, currency="EUR")
print(f"Cost: €{prompt_cost_eur}")
# Cost: €2.7e-06
```

**Counting tokens**
Expand All @@ -118,5 +131,29 @@ Under the hood, strings and ChatML messages are tokenized using [Tiktoken](https
For Anthropic models above version 3 (i.e. Sonnet 3.5, Haiku 3.5, and Opus 3), we use the [Anthropic beta token counting API](https://docs.anthropic.com/claude/docs/beta-api-for-counting-tokens) to ensure accurate token counts. For older Claude models, we approximate using Tiktoken with the cl100k_base encoding.


## Multi-Currency Support

TokenCost supports cost calculations in multiple currencies:

```python
from tokencost import get_supported_currencies, calculate_prompt_cost

# Check supported currencies
print(get_supported_currencies())
# ['USD', 'EUR']

# Calculate costs in different currencies
prompt = "Hello world"
model = "gpt-3.5-turbo"

usd_cost = calculate_prompt_cost(prompt, model, currency="USD")
eur_cost = calculate_prompt_cost(prompt, model, currency="EUR")

print(f"USD: ${usd_cost}")
print(f"EUR: €{eur_cost}")
```

Currency conversion uses real-time exchange rates from the European Central Bank, updated daily.

## Cost table
Units denominated in USD. All prices can be located [here](pricing_table.md).
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ classifiers = [
dependencies = [
"tiktoken>=0.9.0",
"aiohttp>=3.9.3",
"anthropic>=0.34.0"
"anthropic>=0.34.0",
"forex-python>=1.6"
]

[project.optional-dependencies]
Expand Down
110 changes: 110 additions & 0 deletions tests/test_costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
calculate_cost_by_tokens,
calculate_prompt_cost,
calculate_completion_cost,
calculate_all_costs_and_tokens,
)
from tokencost.constants import get_supported_currencies

# 15 tokens
MESSAGES = [
Expand Down Expand Up @@ -271,3 +273,111 @@ def test_calculate_cached_tokens_cost():
# Assert that the costs match
assert actual_cost == expected_cost
assert actual_cost > 0, "Cache token cost should be greater than zero"


class TestCurrencySupport:
"""Test currency conversion functionality."""

def test_get_supported_currencies(self):
"""Test that supported currencies are returned correctly."""
currencies = get_supported_currencies()
assert isinstance(currencies, list)
assert "USD" in currencies
assert "EUR" in currencies

def test_calculate_prompt_cost_eur(self):
"""Test prompt cost calculation in EUR."""
prompt = "Hello world"
model = "gpt-3.5-turbo"

usd_cost = calculate_prompt_cost(prompt, model, currency="USD")
eur_cost = calculate_prompt_cost(prompt, model, currency="EUR")

assert isinstance(usd_cost, Decimal)
assert isinstance(eur_cost, Decimal)
assert usd_cost > 0
assert eur_cost > 0
# EUR cost should be different from USD cost (unless exchange rate is exactly 1.0)
assert usd_cost != eur_cost or abs(usd_cost - eur_cost) < Decimal("0.000001")

def test_calculate_completion_cost_eur(self):
"""Test completion cost calculation in EUR."""
completion = "Hello world"
model = "gpt-3.5-turbo"

usd_cost = calculate_completion_cost(completion, model, currency="USD")
eur_cost = calculate_completion_cost(completion, model, currency="EUR")

assert isinstance(usd_cost, Decimal)
assert isinstance(eur_cost, Decimal)
assert usd_cost > 0
assert eur_cost > 0

def test_calculate_cost_by_tokens_eur(self):
"""Test cost calculation by tokens in EUR."""
num_tokens = 100
model = "gpt-3.5-turbo"
token_type = "input"

usd_cost = calculate_cost_by_tokens(num_tokens, model, token_type, currency="USD")
eur_cost = calculate_cost_by_tokens(num_tokens, model, token_type, currency="EUR")

assert isinstance(usd_cost, Decimal)
assert isinstance(eur_cost, Decimal)
assert usd_cost > 0
assert eur_cost > 0

def test_calculate_all_costs_and_tokens_eur(self):
"""Test all costs and tokens calculation in EUR."""
prompt = "Hello world"
completion = "Hi there!"
model = "gpt-3.5-turbo"

usd_result = calculate_all_costs_and_tokens(prompt, completion, model, currency="USD")
eur_result = calculate_all_costs_and_tokens(prompt, completion, model, currency="EUR")

# Check structure
for result in [usd_result, eur_result]:
assert "prompt_cost" in result
assert "prompt_tokens" in result
assert "completion_cost" in result
assert "completion_tokens" in result
assert isinstance(result["prompt_cost"], Decimal)
assert isinstance(result["completion_cost"], Decimal)
assert isinstance(result["prompt_tokens"], int)
assert isinstance(result["completion_tokens"], int)

# Token counts should be the same
assert usd_result["prompt_tokens"] == eur_result["prompt_tokens"]
assert usd_result["completion_tokens"] == eur_result["completion_tokens"]

def test_currency_case_insensitive(self):
"""Test that currency parameter is case insensitive."""
prompt = "Hello world"
model = "gpt-3.5-turbo"

eur_upper = calculate_prompt_cost(prompt, model, currency="EUR")
eur_lower = calculate_prompt_cost(prompt, model, currency="eur")

assert eur_upper == eur_lower

def test_invalid_currency_fallback(self):
"""Test that invalid currency falls back to USD."""
prompt = "Hello world"
model = "gpt-3.5-turbo"

usd_cost = calculate_prompt_cost(prompt, model, currency="USD")
invalid_cost = calculate_prompt_cost(prompt, model, currency="INVALID")

# Should fallback to USD cost
assert usd_cost == invalid_cost

def test_default_currency_is_usd(self):
"""Test that default currency behavior is preserved (USD)."""
prompt = "Hello world"
model = "gpt-3.5-turbo"

default_cost = calculate_prompt_cost(prompt, model)
usd_cost = calculate_prompt_cost(prompt, model, currency="USD")

assert default_cost == usd_cost
2 changes: 1 addition & 1 deletion tokencost/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
calculate_all_costs_and_tokens,
calculate_cost_by_tokens,
)
from .constants import TOKEN_COSTS_STATIC, TOKEN_COSTS, update_token_costs, refresh_prices
from .constants import TOKEN_COSTS_STATIC, TOKEN_COSTS, update_token_costs, refresh_prices, get_supported_currencies
Loading