Skip to content

Commit e709428

Browse files
authored
[Cursor] Add token tracking and cost estimation (#31)
* Improve the planner prompt. * [Cursor] Add token tracking and cost estimation Key changes: - Add TokenTracker module for tracking API token usage and costs - Implement per-day session management for usage tracking - Add support for OpenAI and Claude cost calculations - Update LLM API modules to track token usage - Fix and enhance unit tests across all modules - Add aiohttp for web scraping The token tracking system provides: - Token usage tracking for all LLM API calls - Cost estimation based on provider-specific pricing - Daily session management for usage statistics - Command-line interface for viewing usage summaries * [Cursor] Update token tracker and fix tests * [Cursor] Add multi-agent branch to CI workflow * [Cursor] Remove main branch from CI workflow * [Cursor] Fix plan_exec_llm tests to properly mock OpenAI client
1 parent 81a3cdb commit e709428

14 files changed

+869
-194
lines changed

.cursorrules

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ If needed, you can further use the `web_scraper.py` file to scrape the web page
125125
- For search results, ensure proper handling of different character encodings (UTF-8) for international queries
126126
- Add debug information to stderr while keeping the main output clean in stdout for better pipeline integration
127127
- When using seaborn styles in matplotlib, use 'seaborn-v0_8' instead of 'seaborn' as the style name due to recent seaborn version changes
128-
- Use 'gpt-4o' as the model name for OpenAI's GPT-4 with vision capabilities
128+
- Use `gpt-4o` as the model name for OpenAI. It is the latest GPT model and has vision capabilities as well. `o1` is the most advanced and expensive model from OpenAI. Use it when you need to do reasoning, planning, or get blocked.
129+
- Use `claude-3-5-sonnet-20241022` as the model name for Claude. It is the latest Claude model and has vision capabilities as well.
129130

130131
# Multi-Agent Scratchpad
131132

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Unit Tests
22

33
on:
44
pull_request:
5-
branches: [ master, main ]
5+
branches: [ master, multi-agent ]
66
push:
7-
branches: [ master, main ]
7+
branches: [ master, multi-agent ]
88

99
jobs:
1010
test:
@@ -34,4 +34,4 @@ jobs:
3434
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
3535
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
3636
run: |
37-
PYTHONPATH=. python -m unittest discover tests/
37+
PYTHONPATH=. pytest tests/

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@ credentials.json
5959

6060
# vscode
6161
.vscode/
62+
63+
# Token tracking logs
64+
token_logs/
65+
test_token_logs/

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ python -m playwright install chromium
100100
- Search engine integration (DuckDuckGo)
101101
- LLM-powered text analysis
102102
- Process planning and self-reflection capabilities
103+
- Token and cost tracking for LLM API calls
104+
- Supports OpenAI (o1, gpt-4o) and Anthropic (Claude-3.5) models
105+
- Tracks token usage, costs, and thinking time
106+
- Provides session-based tracking with detailed statistics
107+
- Command-line interface for viewing usage statistics
103108

104109
## Testing
105110

requirements.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,18 @@ google-generativeai
2020

2121
# gRPC, for Google Generative AI preventing WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
2222
grpcio==1.60.1
23+
24+
# Financial data and visualization
25+
yfinance>=0.2.36
26+
pandas>=2.1.4
27+
matplotlib>=3.8.2
28+
seaborn>=0.13.1
29+
30+
# UUID
31+
uuid
32+
33+
# Tabulate for pretty-printing tables
34+
tabulate
35+
36+
# Added from the code block
37+
aiohttp==3.9.3

tests/test_llm_api.py

Lines changed: 50 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
11
import unittest
22
from unittest.mock import patch, MagicMock, mock_open
33
from tools.llm_api import create_llm_client, query_llm, load_environment
4+
from tools.token_tracker import TokenUsage, APIResponse
45
import os
56
import google.generativeai as genai
67
import io
78
import sys
89

9-
def is_llm_configured():
10-
"""Check if LLM is configured by trying to connect to the server"""
11-
try:
12-
client = create_llm_client()
13-
response = query_llm("test", client)
14-
return response is not None
15-
except:
16-
return False
17-
18-
# Skip all LLM tests if LLM is not configured
19-
skip_llm_tests = not is_llm_configured()
20-
skip_message = "Skipping LLM tests as LLM is not configured. This is normal if you haven't set up a local LLM server."
21-
2210
class TestEnvironmentLoading(unittest.TestCase):
2311
def setUp(self):
2412
# Save original environment
@@ -87,23 +75,43 @@ def setUp(self):
8775
# Create mock clients for different providers
8876
self.mock_openai_client = MagicMock()
8977
self.mock_anthropic_client = MagicMock()
78+
self.mock_azure_client = MagicMock()
9079
self.mock_gemini_client = MagicMock()
9180

92-
# Set up OpenAI-style response
81+
# Set up mock responses
9382
self.mock_openai_response = MagicMock()
94-
self.mock_openai_choice = MagicMock()
95-
self.mock_openai_message = MagicMock()
96-
self.mock_openai_message.content = "Test OpenAI response"
97-
self.mock_openai_choice.message = self.mock_openai_message
98-
self.mock_openai_response.choices = [self.mock_openai_choice]
99-
self.mock_openai_client.chat.completions.create.return_value = self.mock_openai_response
100-
101-
# Set up Anthropic-style response
83+
self.mock_openai_response.choices = [MagicMock()]
84+
self.mock_openai_response.choices[0].message = MagicMock()
85+
self.mock_openai_response.choices[0].message.content = "Test OpenAI response"
86+
self.mock_openai_response.usage = TokenUsage(
87+
prompt_tokens=10,
88+
completion_tokens=5,
89+
total_tokens=15,
90+
reasoning_tokens=None
91+
)
92+
10293
self.mock_anthropic_response = MagicMock()
103-
self.mock_anthropic_content = MagicMock()
104-
self.mock_anthropic_content.text = "Test Anthropic response"
105-
self.mock_anthropic_response.content = [self.mock_anthropic_content]
94+
self.mock_anthropic_response.content = [MagicMock()]
95+
self.mock_anthropic_response.content[0].text = "Test Anthropic response"
96+
self.mock_anthropic_response.usage = MagicMock()
97+
self.mock_anthropic_response.usage.input_tokens = 10
98+
self.mock_anthropic_response.usage.output_tokens = 5
99+
100+
self.mock_azure_response = MagicMock()
101+
self.mock_azure_response.choices = [MagicMock()]
102+
self.mock_azure_response.choices[0].message = MagicMock()
103+
self.mock_azure_response.choices[0].message.content = "Test Azure OpenAI response"
104+
self.mock_azure_response.usage = TokenUsage(
105+
prompt_tokens=10,
106+
completion_tokens=5,
107+
total_tokens=15,
108+
reasoning_tokens=None
109+
)
110+
111+
# Set up return values for mock clients
112+
self.mock_openai_client.chat.completions.create.return_value = self.mock_openai_response
106113
self.mock_anthropic_client.messages.create.return_value = self.mock_anthropic_response
114+
self.mock_azure_client.chat.completions.create.return_value = self.mock_azure_response
107115

108116
# Set up Gemini-style response
109117
self.mock_gemini_model = MagicMock()
@@ -122,29 +130,17 @@ def setUp(self):
122130
'AZURE_OPENAI_MODEL_DEPLOYMENT': 'test-model-deployment'
123131
})
124132
self.env_patcher.start()
125-
126-
# Set up Azure OpenAI mock
127-
self.mock_azure_response = MagicMock()
128-
self.mock_azure_choice = MagicMock()
129-
self.mock_azure_message = MagicMock()
130-
self.mock_azure_message.content = "Test Azure OpenAI response"
131-
self.mock_azure_choice.message = self.mock_azure_message
132-
self.mock_azure_response.choices = [self.mock_azure_choice]
133-
self.mock_azure_client = MagicMock()
134-
self.mock_azure_client.chat.completions.create.return_value = self.mock_azure_response
135133

136134
def tearDown(self):
137135
self.env_patcher.stop()
138136

139-
@unittest.skipIf(skip_llm_tests, skip_message)
140137
@patch('tools.llm_api.OpenAI')
141138
def test_create_openai_client(self, mock_openai):
142139
mock_openai.return_value = self.mock_openai_client
143140
client = create_llm_client("openai")
144141
mock_openai.assert_called_once_with(api_key='test-openai-key')
145142
self.assertEqual(client, self.mock_openai_client)
146143

147-
@unittest.skipIf(skip_llm_tests, skip_message)
148144
@patch('tools.llm_api.AzureOpenAI')
149145
def test_create_azure_client(self, mock_azure):
150146
mock_azure.return_value = self.mock_azure_client
@@ -156,7 +152,6 @@ def test_create_azure_client(self, mock_azure):
156152
)
157153
self.assertEqual(client, self.mock_azure_client)
158154

159-
@unittest.skipIf(skip_llm_tests, skip_message)
160155
@patch('tools.llm_api.OpenAI')
161156
def test_create_deepseek_client(self, mock_openai):
162157
mock_openai.return_value = self.mock_openai_client
@@ -167,86 +162,67 @@ def test_create_deepseek_client(self, mock_openai):
167162
)
168163
self.assertEqual(client, self.mock_openai_client)
169164

170-
@unittest.skipIf(skip_llm_tests, skip_message)
171165
@patch('tools.llm_api.Anthropic')
172166
def test_create_anthropic_client(self, mock_anthropic):
173167
mock_anthropic.return_value = self.mock_anthropic_client
174168
client = create_llm_client("anthropic")
175169
mock_anthropic.assert_called_once_with(api_key='test-anthropic-key')
176170
self.assertEqual(client, self.mock_anthropic_client)
177171

178-
@unittest.skipIf(skip_llm_tests, skip_message)
179172
@patch('tools.llm_api.genai')
180173
def test_create_gemini_client(self, mock_genai):
181174
client = create_llm_client("gemini")
182175
mock_genai.configure.assert_called_once_with(api_key='test-google-key')
183176
self.assertEqual(client, mock_genai)
184177

185-
@unittest.skipIf(skip_llm_tests, skip_message)
186-
@patch('tools.llm_api.OpenAI')
187-
def test_create_local_client(self, mock_openai):
188-
mock_openai.return_value = self.mock_openai_client
189-
client = create_llm_client("local")
190-
mock_openai.assert_called_once_with(
191-
base_url="http://192.168.180.137:8006/v1",
192-
api_key="not-needed"
193-
)
194-
self.assertEqual(client, self.mock_openai_client)
195-
196-
@unittest.skipIf(skip_llm_tests, skip_message)
197178
def test_create_invalid_provider(self):
198179
with self.assertRaises(ValueError):
199180
create_llm_client("invalid_provider")
200181

201-
@unittest.skipIf(skip_llm_tests, skip_message)
202-
@patch('tools.llm_api.create_llm_client')
182+
@patch('tools.llm_api.OpenAI')
203183
def test_query_openai(self, mock_create_client):
204184
mock_create_client.return_value = self.mock_openai_client
205-
response = query_llm("Test prompt", provider="openai")
185+
response = query_llm("Test prompt", provider="openai", model="gpt-4o")
206186
self.assertEqual(response, "Test OpenAI response")
207187
self.mock_openai_client.chat.completions.create.assert_called_once_with(
208188
model="gpt-4o",
209189
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}],
210190
temperature=0.7
211191
)
212192

213-
@unittest.skipIf(skip_llm_tests, skip_message)
214193
@patch('tools.llm_api.create_llm_client')
215194
def test_query_azure(self, mock_create_client):
216195
mock_create_client.return_value = self.mock_azure_client
217-
response = query_llm("Test prompt", provider="azure")
196+
response = query_llm("Test prompt", provider="azure", model="gpt-4o")
218197
self.assertEqual(response, "Test Azure OpenAI response")
219198
self.mock_azure_client.chat.completions.create.assert_called_once_with(
220-
model=os.getenv('AZURE_OPENAI_MODEL_DEPLOYMENT', 'gpt-4o-ms'),
199+
model="gpt-4o",
221200
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}],
222201
temperature=0.7
223202
)
224203

225-
@unittest.skipIf(skip_llm_tests, skip_message)
226204
@patch('tools.llm_api.create_llm_client')
227205
def test_query_deepseek(self, mock_create_client):
228206
mock_create_client.return_value = self.mock_openai_client
229-
response = query_llm("Test prompt", provider="deepseek")
207+
response = query_llm("Test prompt", provider="deepseek", model="gpt-4o")
230208
self.assertEqual(response, "Test OpenAI response")
231209
self.mock_openai_client.chat.completions.create.assert_called_once_with(
232-
model="deepseek-chat",
210+
model="gpt-4o",
233211
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}],
234212
temperature=0.7
235213
)
236214

237-
@unittest.skipIf(skip_llm_tests, skip_message)
238215
@patch('tools.llm_api.create_llm_client')
239216
def test_query_anthropic(self, mock_create_client):
240217
mock_create_client.return_value = self.mock_anthropic_client
241-
response = query_llm("Test prompt", provider="anthropic")
218+
response = query_llm("Test prompt", provider="anthropic", model="claude-3-5-sonnet-20241022")
242219
self.assertEqual(response, "Test Anthropic response")
243220
self.mock_anthropic_client.messages.create.assert_called_once_with(
244-
model="claude-3-sonnet-20240229",
221+
model="claude-3-5-sonnet-20241022",
245222
max_tokens=1000,
246223
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}]
247224
)
248225

249-
@unittest.skipIf(skip_llm_tests, skip_message)
250226
@patch('tools.llm_api.create_llm_client')
251227
def test_query_gemini(self, mock_create_client):
252228
mock_create_client.return_value = self.mock_gemini_client
@@ -255,35 +231,21 @@ def test_query_gemini(self, mock_create_client):
255231
self.mock_gemini_client.GenerativeModel.assert_called_once_with("gemini-pro")
256232
self.mock_gemini_model.generate_content.assert_called_once_with("Test prompt")
257233

258-
@unittest.skipIf(skip_llm_tests, skip_message)
259-
@patch('tools.llm_api.create_llm_client')
260-
def test_query_local(self, mock_create_client):
261-
mock_create_client.return_value = self.mock_openai_client
262-
response = query_llm("Test prompt", provider="local")
263-
self.assertEqual(response, "Test OpenAI response")
264-
self.mock_openai_client.chat.completions.create.assert_called_once_with(
265-
model="Qwen/Qwen2.5-32B-Instruct-AWQ",
266-
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}],
267-
temperature=0.7
268-
)
269-
270-
@unittest.skipIf(skip_llm_tests, skip_message)
271234
@patch('tools.llm_api.create_llm_client')
272235
def test_query_with_custom_model(self, mock_create_client):
273236
mock_create_client.return_value = self.mock_openai_client
274-
response = query_llm("Test prompt", model="custom-model")
237+
response = query_llm("Test prompt", provider="openai", model="gpt-4o")
275238
self.assertEqual(response, "Test OpenAI response")
276239
self.mock_openai_client.chat.completions.create.assert_called_once_with(
277-
model="custom-model",
240+
model="gpt-4o",
278241
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}],
279242
temperature=0.7
280243
)
281244

282-
@unittest.skipIf(skip_llm_tests, skip_message)
283245
@patch('tools.llm_api.create_llm_client')
284246
def test_query_o1_model(self, mock_create_client):
285247
mock_create_client.return_value = self.mock_openai_client
286-
response = query_llm("Test prompt", model="o1")
248+
response = query_llm("Test prompt", provider="openai", model="o1")
287249
self.assertEqual(response, "Test OpenAI response")
288250
self.mock_openai_client.chat.completions.create.assert_called_once_with(
289251
model="o1",
@@ -292,14 +254,16 @@ def test_query_o1_model(self, mock_create_client):
292254
reasoning_effort="low"
293255
)
294256

295-
@unittest.skipIf(skip_llm_tests, skip_message)
296257
@patch('tools.llm_api.create_llm_client')
297258
def test_query_with_existing_client(self, mock_create_client):
298-
response = query_llm("Test prompt", client=self.mock_openai_client)
259+
response = query_llm("Test prompt", client=self.mock_openai_client, model="gpt-4o")
299260
self.assertEqual(response, "Test OpenAI response")
300-
mock_create_client.assert_not_called()
261+
self.mock_openai_client.chat.completions.create.assert_called_once_with(
262+
model="gpt-4o",
263+
messages=[{"role": "user", "content": [{"type": "text", "text": "Test prompt"}]}],
264+
temperature=0.7
265+
)
301266

302-
@unittest.skipIf(skip_llm_tests, skip_message)
303267
@patch('tools.llm_api.create_llm_client')
304268
def test_query_error(self, mock_create_client):
305269
self.mock_openai_client.chat.completions.create.side_effect = Exception("Test error")

0 commit comments

Comments
 (0)