llm-cost-tracker 0.1.2__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Heondeuk Lee
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.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-cost-tracker
3
+ Version: 0.1.2
4
+ Summary: LLM API call token usage-based expense tracker
5
+ Home-page: https://github.com/hundredeuk2/tracker
6
+ Author: Heondeuk Lee
7
+ Author-email: leehd1995@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: PyYAML>=5.1
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license
23
+ Dynamic: license-file
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ # TrackMyLLM(cost)
29
+
30
+ ### 1. Example usage for class method:
31
+ ```python
32
+ from tracker.cost_tracker import cost_tracker
33
+ from openai import OpenAI, AsyncOpenAI
34
+
35
+ class Agent:
36
+ def __init__(self, model_name, api_key = None):
37
+ self.model_name = model_name # nessary
38
+ self.costs: dict[str, list[float]] = {} # nessary
39
+
40
+ if api_key:
41
+ self._initialize_client(api_key)
42
+
43
+ def _initialize_client(self, api_key):
44
+ self.client = OpenAI(api_key=api_key)
45
+ self.aclient = AsyncOpenAI(api_key=api_key)
46
+
47
+ # just for compatibility
48
+ def total_cost(self):
49
+ return float(round(sum(sum(lst) for lst in self.costs.values()), 6))
50
+
51
+ @cost_tracker.track_cost()
52
+ def ask(self, prompt: str):
53
+ resp = self.client.chat.completions.create(
54
+ model=self.model_name,
55
+ messages=[{"role":"user","content":prompt}],
56
+ )
57
+ return resp, {"model_response": resp.choices[0].message.content}
58
+
59
+ @cost_tracker.track_cost()
60
+ async def aask(self, prompt: str):
61
+ resp = await self.aclient.ChatCompletion.acreate(
62
+ model=self.model_name,
63
+ messages=[{"role":"user","content":prompt}],
64
+ )
65
+ return resp, {"model_response": resp.choices[0].message.content}
66
+
67
+ test_client = Agent(model_name="gpt-4o-mini")
68
+ a, b = test_client.ask("Hello, world!")
69
+ print("Individual costs: ", test_client.costs)
70
+ print("Total cost: ", format(test_client.total_cost(), "f"))
71
+
72
+ # Individual costs: defaultdict(<class 'list'>, {'gpt-4o-mini': [8.400000000000001e-06]})
73
+ # Total cost: 0.000008
74
+
75
+ a, b = await test_client.aask("Hello, world!")
76
+ print("Individual costs: ", test_client.costs)
77
+ print("Total cost: ", format(test_client.total_cost(), "f"))
78
+
79
+ # Individual costs: {'gpt-4o-mini': [7.65e-06, 7.65e-06]}
80
+ # Total cost: 0.000015
81
+ ```
82
+
83
+ ### 2. Example usage for single function:
84
+ ```python
85
+ from openai import OpenAI
86
+ from tracker.cost_tracker import cost_tracker
87
+
88
+ model_name = "gpt-4o-mini"
89
+ client = OpenAI()
90
+
91
+ @cost_tracker.track_cost()
92
+ def generate(model_name, prompt):
93
+ completion = client.chat.completions.create(
94
+ model = model_name,
95
+ max_tokens=8192,
96
+ messages=[
97
+ {
98
+ "role":"system",
99
+ "content":"Your a Great AI"
100
+ },
101
+ {
102
+ "role":"user",
103
+ "content":prompt
104
+ }
105
+ ],
106
+ )
107
+ return completion
108
+
109
+ response = generate(model_name, "Hello, world!")
110
+ print("Individual costs: ", cost_tracker.costs)
111
+ print("Total cost: ", format(cost_tracker.total_cost(), "f"))
112
+
113
+ # Individual costs: defaultdict(<class 'list'>, {'gpt-4o-mini': [8.400000000000001e-06]})
114
+ # Total cost: 0.000008
115
+ ```
@@ -0,0 +1,88 @@
1
+ # TrackMyLLM(cost)
2
+
3
+ ### 1. Example usage for class method:
4
+ ```python
5
+ from tracker.cost_tracker import cost_tracker
6
+ from openai import OpenAI, AsyncOpenAI
7
+
8
+ class Agent:
9
+ def __init__(self, model_name, api_key = None):
10
+ self.model_name = model_name # nessary
11
+ self.costs: dict[str, list[float]] = {} # nessary
12
+
13
+ if api_key:
14
+ self._initialize_client(api_key)
15
+
16
+ def _initialize_client(self, api_key):
17
+ self.client = OpenAI(api_key=api_key)
18
+ self.aclient = AsyncOpenAI(api_key=api_key)
19
+
20
+ # just for compatibility
21
+ def total_cost(self):
22
+ return float(round(sum(sum(lst) for lst in self.costs.values()), 6))
23
+
24
+ @cost_tracker.track_cost()
25
+ def ask(self, prompt: str):
26
+ resp = self.client.chat.completions.create(
27
+ model=self.model_name,
28
+ messages=[{"role":"user","content":prompt}],
29
+ )
30
+ return resp, {"model_response": resp.choices[0].message.content}
31
+
32
+ @cost_tracker.track_cost()
33
+ async def aask(self, prompt: str):
34
+ resp = await self.aclient.ChatCompletion.acreate(
35
+ model=self.model_name,
36
+ messages=[{"role":"user","content":prompt}],
37
+ )
38
+ return resp, {"model_response": resp.choices[0].message.content}
39
+
40
+ test_client = Agent(model_name="gpt-4o-mini")
41
+ a, b = test_client.ask("Hello, world!")
42
+ print("Individual costs: ", test_client.costs)
43
+ print("Total cost: ", format(test_client.total_cost(), "f"))
44
+
45
+ # Individual costs: defaultdict(<class 'list'>, {'gpt-4o-mini': [8.400000000000001e-06]})
46
+ # Total cost: 0.000008
47
+
48
+ a, b = await test_client.aask("Hello, world!")
49
+ print("Individual costs: ", test_client.costs)
50
+ print("Total cost: ", format(test_client.total_cost(), "f"))
51
+
52
+ # Individual costs: {'gpt-4o-mini': [7.65e-06, 7.65e-06]}
53
+ # Total cost: 0.000015
54
+ ```
55
+
56
+ ### 2. Example usage for single function:
57
+ ```python
58
+ from openai import OpenAI
59
+ from tracker.cost_tracker import cost_tracker
60
+
61
+ model_name = "gpt-4o-mini"
62
+ client = OpenAI()
63
+
64
+ @cost_tracker.track_cost()
65
+ def generate(model_name, prompt):
66
+ completion = client.chat.completions.create(
67
+ model = model_name,
68
+ max_tokens=8192,
69
+ messages=[
70
+ {
71
+ "role":"system",
72
+ "content":"Your a Great AI"
73
+ },
74
+ {
75
+ "role":"user",
76
+ "content":prompt
77
+ }
78
+ ],
79
+ )
80
+ return completion
81
+
82
+ response = generate(model_name, "Hello, world!")
83
+ print("Individual costs: ", cost_tracker.costs)
84
+ print("Total cost: ", format(cost_tracker.total_cost(), "f"))
85
+
86
+ # Individual costs: defaultdict(<class 'list'>, {'gpt-4o-mini': [8.400000000000001e-06]})
87
+ # Total cost: 0.000008
88
+ ```
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-cost-tracker
3
+ Version: 0.1.2
4
+ Summary: LLM API call token usage-based expense tracker
5
+ Home-page: https://github.com/hundredeuk2/tracker
6
+ Author: Heondeuk Lee
7
+ Author-email: leehd1995@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: PyYAML>=5.1
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license
23
+ Dynamic: license-file
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ # TrackMyLLM(cost)
29
+
30
+ ### 1. Example usage for class method:
31
+ ```python
32
+ from tracker.cost_tracker import cost_tracker
33
+ from openai import OpenAI, AsyncOpenAI
34
+
35
+ class Agent:
36
+ def __init__(self, model_name, api_key = None):
37
+ self.model_name = model_name # nessary
38
+ self.costs: dict[str, list[float]] = {} # nessary
39
+
40
+ if api_key:
41
+ self._initialize_client(api_key)
42
+
43
+ def _initialize_client(self, api_key):
44
+ self.client = OpenAI(api_key=api_key)
45
+ self.aclient = AsyncOpenAI(api_key=api_key)
46
+
47
+ # just for compatibility
48
+ def total_cost(self):
49
+ return float(round(sum(sum(lst) for lst in self.costs.values()), 6))
50
+
51
+ @cost_tracker.track_cost()
52
+ def ask(self, prompt: str):
53
+ resp = self.client.chat.completions.create(
54
+ model=self.model_name,
55
+ messages=[{"role":"user","content":prompt}],
56
+ )
57
+ return resp, {"model_response": resp.choices[0].message.content}
58
+
59
+ @cost_tracker.track_cost()
60
+ async def aask(self, prompt: str):
61
+ resp = await self.aclient.ChatCompletion.acreate(
62
+ model=self.model_name,
63
+ messages=[{"role":"user","content":prompt}],
64
+ )
65
+ return resp, {"model_response": resp.choices[0].message.content}
66
+
67
+ test_client = Agent(model_name="gpt-4o-mini")
68
+ a, b = test_client.ask("Hello, world!")
69
+ print("Individual costs: ", test_client.costs)
70
+ print("Total cost: ", format(test_client.total_cost(), "f"))
71
+
72
+ # Individual costs: defaultdict(<class 'list'>, {'gpt-4o-mini': [8.400000000000001e-06]})
73
+ # Total cost: 0.000008
74
+
75
+ a, b = await test_client.aask("Hello, world!")
76
+ print("Individual costs: ", test_client.costs)
77
+ print("Total cost: ", format(test_client.total_cost(), "f"))
78
+
79
+ # Individual costs: {'gpt-4o-mini': [7.65e-06, 7.65e-06]}
80
+ # Total cost: 0.000015
81
+ ```
82
+
83
+ ### 2. Example usage for single function:
84
+ ```python
85
+ from openai import OpenAI
86
+ from tracker.cost_tracker import cost_tracker
87
+
88
+ model_name = "gpt-4o-mini"
89
+ client = OpenAI()
90
+
91
+ @cost_tracker.track_cost()
92
+ def generate(model_name, prompt):
93
+ completion = client.chat.completions.create(
94
+ model = model_name,
95
+ max_tokens=8192,
96
+ messages=[
97
+ {
98
+ "role":"system",
99
+ "content":"Your a Great AI"
100
+ },
101
+ {
102
+ "role":"user",
103
+ "content":prompt
104
+ }
105
+ ],
106
+ )
107
+ return completion
108
+
109
+ response = generate(model_name, "Hello, world!")
110
+ print("Individual costs: ", cost_tracker.costs)
111
+ print("Total cost: ", format(cost_tracker.total_cost(), "f"))
112
+
113
+ # Individual costs: defaultdict(<class 'list'>, {'gpt-4o-mini': [8.400000000000001e-06]})
114
+ # Total cost: 0.000008
115
+ ```
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ llm_cost_tracker.egg-info/PKG-INFO
5
+ llm_cost_tracker.egg-info/SOURCES.txt
6
+ llm_cost_tracker.egg-info/dependency_links.txt
7
+ llm_cost_tracker.egg-info/requires.txt
8
+ llm_cost_tracker.egg-info/top_level.txt
9
+ tracker/__init__.py
10
+ tracker/cost_tracker.py
11
+ tracker/pricing.yaml
12
+ tracker/pricing_loader.py
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from setuptools import setup, find_packages
2
+ from pathlib import Path
3
+
4
+ README = Path(__file__).parent / "README.md"
5
+
6
+ setup(
7
+ name="llm-cost-tracker",
8
+ version="0.1.2",
9
+ description="LLM API call token usage-based expense tracker",
10
+ long_description=README.read_text(encoding="utf-8"),
11
+ long_description_content_type="text/markdown",
12
+ author="Heondeuk Lee",
13
+ author_email="leehd1995@gmail.com",
14
+ url="https://github.com/hundredeuk2/tracker",
15
+ packages=find_packages(exclude=["tutorial", "tests*"]),
16
+ install_requires=[
17
+ "PyYAML>=5.1",
18
+ ],
19
+ python_requires=">=3.10",
20
+ classifiers=[
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ],
25
+ include_package_data=True,
26
+ package_data={
27
+ "tracker": ["pricing.yaml"],
28
+ },
29
+ license="MIT",
30
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,94 @@
1
+ import inspect
2
+ from functools import wraps
3
+ from collections import defaultdict
4
+ from typing import Any, Callable
5
+ from .pricing_loader import load_pricing_yaml
6
+
7
+ class CostTracker:
8
+ def __init__(self,
9
+ pricing: dict[str, dict[str, float]] = None,
10
+ pricing_path: str = "pricing.yaml"):
11
+ self.pricing = pricing or load_pricing_yaml(pricing_path)
12
+ self.costs: dict[str, list[float]] = defaultdict(list)
13
+
14
+ def total_cost(self, instance: Any = None) -> float:
15
+ if instance is not None and hasattr(instance, "costs"):
16
+ data = instance.costs.values()
17
+ else:
18
+ data = self.costs.values()
19
+ return round(sum(sum(lst) for lst in data), 6)
20
+
21
+ def track_cost(self, response_index: int = 0):
22
+ def decorator(fn: Callable):
23
+ is_async = inspect.iscoroutinefunction(fn)
24
+
25
+ def _calc_cost(resp, pricing):
26
+ usage = getattr(resp, "usage", None)
27
+ pt = next((getattr(usage, attr) for attr in ("prompt_tokens", "input_tokens") if hasattr(usage, attr)), 0)
28
+ ct = next((getattr(usage, attr) for attr in ("completion_tokens", "output_tokens") if hasattr(usage, attr)), 0)
29
+ return pt * pricing.get("prompt", 0.0) + ct * pricing.get("completion", 0.0)
30
+
31
+ if is_async:
32
+ @wraps(fn)
33
+ async def async_wrapper(*args, **kwargs):
34
+ result = await fn(*args, **kwargs)
35
+ resp = (result[response_index]
36
+ if isinstance(result, (tuple, list)) else result)
37
+ inst = args[0] if args else None
38
+
39
+ # 모델 이름 추출 로직은 기존과 동일
40
+ if hasattr(inst, "model_name"):
41
+ model_name = inst.model_name
42
+ elif args:
43
+ model_name = args[0]
44
+ else:
45
+ model_name = None
46
+
47
+ self.check_company(model_name)
48
+
49
+ cost = _calc_cost(resp, self.price_detail[model_name])
50
+ if hasattr(inst, "costs"):
51
+ inst.costs.setdefault(model_name, []).append(cost)
52
+ else:
53
+ self.costs.setdefault(model_name, []).append(cost)
54
+ return result
55
+ return async_wrapper
56
+
57
+ else:
58
+ @wraps(fn)
59
+ def sync_wrapper(*args, **kwargs):
60
+ result = fn(*args, **kwargs)
61
+ resp = (result[response_index]
62
+ if isinstance(result, (tuple, list)) else result)
63
+ inst = args[0] if args else None
64
+
65
+ if hasattr(inst, "model_name"):
66
+ model_name = inst.model_name
67
+ elif args:
68
+ model_name = args[0]
69
+ else:
70
+ model_name = None
71
+
72
+ self.check_company(model_name)
73
+
74
+ cost = _calc_cost(resp, self.price_detail[model_name])
75
+ if hasattr(inst, "costs"):
76
+ inst.costs.setdefault(model_name, []).append(cost)
77
+ else:
78
+ self.costs.setdefault(model_name, []).append(cost)
79
+ return result
80
+ return sync_wrapper
81
+
82
+ return decorator
83
+
84
+ def check_company(self, model_name):
85
+ if "gpt" in model_name or "o1" in model_name or "o3" in model_name or "o4" in model_name :
86
+ self.price_detail = self.pricing["openai"]
87
+
88
+ elif "claude" in model_name:
89
+ self.price_detail = self.pricing["antrophic"]
90
+
91
+ else:
92
+ raise ValueError(f"Unsurppot Model: {model_name}")
93
+
94
+ cost_tracker = CostTracker()
@@ -0,0 +1,134 @@
1
+ openai:
2
+ "gpt-4.1":
3
+ prompt: 0.000002
4
+ completion: 0.000008
5
+
6
+ "gpt-4.1-mini":
7
+ prompt: 0.0000004
8
+ completion: 0.0000016
9
+
10
+ "gpt-4.1-nano":
11
+ prompt: 0.0000001
12
+ completion: 0.0000004
13
+
14
+ "gpt-4.5-preview":
15
+ prompt: 0.000075
16
+ completion: 0.000150
17
+
18
+ "gpt-4o":
19
+ prompt: 0.0000025
20
+ completion: 0.0000100
21
+
22
+ "gpt-4o-audio-preview":
23
+ prompt: 0.0000025
24
+ completion: 0.0000100
25
+
26
+ "gpt-4o-realtime-preview":
27
+ prompt: 0.0000050
28
+ completion: 0.0000200
29
+
30
+ "gpt-4o-mini":
31
+ prompt: 0.00000015
32
+ completion: 0.00000060
33
+
34
+ "gpt-4o-mini-audio-preview":
35
+ prompt: 0.00000015
36
+ completion: 0.00000060
37
+
38
+ "gpt-4o-mini-realtime-preview":
39
+ prompt: 0.00000060
40
+ completion: 0.00000240
41
+
42
+ "o1":
43
+ prompt: 0.0000150
44
+ completion: 0.0000600
45
+
46
+ "o1-pro":
47
+ prompt: 0.0001500
48
+ completion: 0.0006000
49
+
50
+ "o3":
51
+ prompt: 0.0000100
52
+ completion: 0.0000400
53
+
54
+ "o4-mini":
55
+ prompt: 0.0000011
56
+ completion: 0.0000044
57
+
58
+ "o3-mini":
59
+ prompt: 0.0000011
60
+ completion: 0.0000044
61
+
62
+ "o1-mini":
63
+ prompt: 0.0000011
64
+ completion: 0.0000044
65
+
66
+ "gpt-4o-mini-search-preview":
67
+ prompt: 0.00000015
68
+ completion: 0.00000060
69
+
70
+ "gpt-4o-search-preview":
71
+ prompt: 0.0000025
72
+ completion: 0.0000100
73
+
74
+ "computer-use-preview":
75
+ prompt: 0.0000030
76
+ completion: 0.0000120
77
+
78
+ "gpt-image-1":
79
+ prompt: 0.0000050
80
+
81
+ antrophic:
82
+ "claude-3-7-sonnet-20250219":
83
+ prompt: 0.000003
84
+ cache_writes: 0.00000375
85
+ cache_hits: 0.00000030
86
+ completion: 0.000015
87
+
88
+ "claude-3-7-sonnet-latest":
89
+ prompt: 0.000003
90
+ cache_writes: 0.00000375
91
+ cache_hits: 0.00000030
92
+ completion: 0.000015
93
+
94
+ "claude-3-5-sonnet-20241022":
95
+ prompt: 0.000003
96
+ cache_writes: 0.00000375
97
+ cache_hits: 0.00000030
98
+ completion: 0.000015
99
+
100
+ "claude-3-5-sonnet-latest":
101
+ prompt: 0.000003
102
+ cache_writes: 0.00000375
103
+ cache_hits: 0.00000030
104
+ completion: 0.000015
105
+
106
+ "claude-3-5-haiku-20241022":
107
+ prompt: 0.00000080
108
+ cache_writes: 0.00000100
109
+ cache_hits: 0.00000008
110
+ completion: 0.000004
111
+
112
+ "claude-3-5-haiku-latest":
113
+ prompt: 0.00000080
114
+ cache_writes: 0.00000100
115
+ cache_hits: 0.00000008
116
+ completion: 0.000004
117
+
118
+ "claude-3-opus-20240229":
119
+ prompt: 0.000015
120
+ cache_writes: 0.00001875
121
+ cache_hits: 0.00000150
122
+ completion: 0.000075
123
+
124
+ "claude-3-opus-latest":
125
+ prompt: 0.000015
126
+ cache_writes: 0.00001875
127
+ cache_hits: 0.00000150
128
+ completion: 0.000075
129
+
130
+ "claude-3-haiku-20240307":
131
+ prompt: 0.00000025
132
+ cache_writes: 0.00000030
133
+ cache_hits: 0.00000003
134
+ completion: 0.00000125
@@ -0,0 +1,14 @@
1
+ import yaml
2
+ from pathlib import Path
3
+ from typing import Dict
4
+
5
+ BASE_DIR = Path(__file__).parent
6
+ DEFAULT_PRICING_FILE = BASE_DIR / "pricing.yaml"
7
+
8
+ def load_pricing_yaml(path: str | Path | None = None) -> Dict[str, Dict[str, float]]:
9
+ yaml_path = Path(path) if path else DEFAULT_PRICING_FILE
10
+ if not yaml_path.is_absolute():
11
+ yaml_path = BASE_DIR / yaml_path
12
+ with open(yaml_path, encoding="utf-8") as f:
13
+ data = yaml.safe_load(f)
14
+ return data