@@ -144,6 +144,80 @@ def __init__(self) -> None:
144144
145145 assert "ALPHA-7742" in response .content
146146
147+ async def test_tool_call_tracking (self ) -> None :
148+ """Test that tool_call_count and request_count are tracked correctly."""
149+
150+ @safe_tool (timeout = 5 )
151+ async def add_numbers (a : int , b : int ) -> str :
152+ """Add two numbers together."""
153+ return f"The sum is { a + b } "
154+
155+ @safe_tool (timeout = 5 )
156+ async def multiply_numbers (a : int , b : int ) -> str :
157+ """Multiply two numbers together."""
158+ return f"The product is { a * b } "
159+
160+ class MathToolset (SafeToolset ):
161+ def __init__ (self ) -> None :
162+ super ().__init__ (tools = [add_numbers , multiply_numbers ], name = "math" )
163+
164+ agent = FastroAgent (
165+ model = "openai:gpt-4o-mini" ,
166+ system_prompt = "Use the math tools to answer questions. Always use tools, don't calculate yourself." ,
167+ toolsets = [MathToolset ()],
168+ )
169+
170+ response = await agent .run ("What is 5 + 3?" )
171+
172+ # Should have made at least 1 tool call
173+ assert response .tool_call_count >= 1 , f"Expected tool_call_count >= 1, got { response .tool_call_count } "
174+ # Should have made at least 2 requests (initial + after tool response)
175+ assert response .request_count >= 2 , f"Expected request_count >= 2, got { response .request_count } "
176+ # Should have tool call details
177+ assert len (response .tool_calls ) >= 1 , "Expected tool_calls list to have items"
178+ assert response .tool_calls [0 ]["tool_name" ] == "add_numbers"
179+
180+ async def test_multiple_tool_calls_tracking (self ) -> None :
181+ """Test tracking when agent makes multiple tool calls."""
182+
183+ @safe_tool (timeout = 5 )
184+ async def get_price (item : str ) -> str :
185+ """Get the price of an item."""
186+ prices = {"apple" : 1.50 , "banana" : 0.75 , "orange" : 2.00 }
187+ return f"{ item } costs ${ prices .get (item , 0 ):.2f} "
188+
189+ class PriceToolset (SafeToolset ):
190+ def __init__ (self ) -> None :
191+ super ().__init__ (tools = [get_price ], name = "prices" )
192+
193+ agent = FastroAgent (
194+ model = "openai:gpt-4o-mini" ,
195+ system_prompt = "Use the get_price tool to answer price questions. Call the tool for each item." ,
196+ toolsets = [PriceToolset ()],
197+ )
198+
199+ response = await agent .run ("How much do an apple and a banana cost?" )
200+
201+ # Should have made at least 2 tool calls (one for each item)
202+ # Note: The model might call them in one go or separately
203+ assert response .tool_call_count >= 1 , f"Expected tool_call_count >= 1, got { response .tool_call_count } "
204+ assert response .request_count >= 2 , f"Expected request_count >= 2, got { response .request_count } "
205+
206+ async def test_usage_details_populated (self ) -> None :
207+ """Test that usage_details dict is populated (for models that provide it)."""
208+ agent = FastroAgent (
209+ model = "openai:gpt-4o-mini" ,
210+ system_prompt = "Be concise." ,
211+ )
212+
213+ response = await agent .run ("Say hello" )
214+
215+ # usage_details should exist (may be empty for some models)
216+ assert isinstance (response .usage_details , dict )
217+ # Basic usage should always be tracked
218+ assert response .input_tokens > 0
219+ assert response .output_tokens > 0
220+
147221
148222class TestSafeToolIntegration :
149223 """Integration tests for @safe_tool with real operations."""
@@ -1218,3 +1292,112 @@ async def execute(self, context: StepContext[None]) -> str:
12181292 )
12191293 assert not result3 .stopped_early
12201294 assert result3 .output == "Order processed for John"
1295+
1296+
1297+ # ============================================================================
1298+ # Cache Token Integration Tests
1299+ # ============================================================================
1300+
1301+
1302+ @requires_openai
1303+ class TestCacheTokensIntegration :
1304+ """Integration tests for cache token tracking with OpenAI.
1305+
1306+ OpenAI supports prompt caching on gpt-4o models for prompts >= 1024 tokens.
1307+ """
1308+
1309+ async def test_cache_fields_populated (self ) -> None :
1310+ """Test that cache token fields exist in response (may be 0 for short prompts)."""
1311+ agent = FastroAgent (
1312+ model = "openai:gpt-4o-mini" ,
1313+ system_prompt = "Be concise." ,
1314+ )
1315+
1316+ response = await agent .run ("Say hello" )
1317+
1318+ # Fields should exist (may be 0 for prompts below caching threshold)
1319+ assert isinstance (response .cache_read_tokens , int )
1320+ assert isinstance (response .cache_write_tokens , int )
1321+ assert response .cache_read_tokens >= 0
1322+ assert response .cache_write_tokens >= 0
1323+
1324+ async def test_request_count_single_call (self ) -> None :
1325+ """Test request_count is 1 for a simple call without tools."""
1326+ agent = FastroAgent (
1327+ model = "openai:gpt-4o-mini" ,
1328+ system_prompt = "Be concise." ,
1329+ )
1330+
1331+ response = await agent .run ("Say hello" )
1332+
1333+ # Should be exactly 1 request for a simple call
1334+ assert response .request_count == 1
1335+
1336+ async def test_cache_tokens_with_long_prompt (self ) -> None :
1337+ """Test cache tokens with a long system prompt that may trigger caching.
1338+
1339+ OpenAI requires prompts >= 1024 tokens for prompt caching to be eligible.
1340+ This test uses a long prompt and makes multiple calls to see if caching occurs.
1341+ """
1342+ # Create a long system prompt (aim for ~1500 tokens)
1343+ long_context = " " .join ([f"Rule { i } : Always be helpful, accurate, and informative." for i in range (200 )])
1344+ system_prompt = f"""You are an expert assistant with the following comprehensive guidelines:
1345+
1346+ { long_context }
1347+
1348+ Remember to always be concise in your final responses."""
1349+
1350+ agent = FastroAgent (
1351+ model = "openai:gpt-4o-mini" ,
1352+ system_prompt = system_prompt ,
1353+ )
1354+
1355+ # First call
1356+ response1 = await agent .run ("Say 'hello'" )
1357+ assert response1 .input_tokens > 0
1358+
1359+ # Second call with same system prompt
1360+ response2 = await agent .run ("Say 'world'" )
1361+ assert response2 .input_tokens > 0
1362+
1363+ # Print for debugging
1364+ print (f"\n Response 1: input={ response1 .input_tokens } , cache_read={ response1 .cache_read_tokens } " )
1365+ print (f"Response 2: input={ response2 .input_tokens } , cache_read={ response2 .cache_read_tokens } " )
1366+
1367+ # Verify cache fields are integers (caching may or may not occur)
1368+ assert isinstance (response1 .cache_read_tokens , int )
1369+ assert isinstance (response2 .cache_read_tokens , int )
1370+
1371+ # Both should have valid costs
1372+ assert response1 .cost_microcents > 0
1373+ assert response2 .cost_microcents > 0
1374+
1375+ async def test_all_new_fields_populated (self ) -> None :
1376+ """Test that all new usage fields are properly populated."""
1377+ agent = FastroAgent (
1378+ model = "openai:gpt-4o-mini" ,
1379+ system_prompt = "Be concise." ,
1380+ )
1381+
1382+ response = await agent .run ("What is 2+2?" )
1383+
1384+ # All new fields should be populated with valid values
1385+ assert isinstance (response .cache_read_tokens , int ) and response .cache_read_tokens >= 0
1386+ assert isinstance (response .cache_write_tokens , int ) and response .cache_write_tokens >= 0
1387+ assert isinstance (response .input_audio_tokens , int ) and response .input_audio_tokens >= 0
1388+ assert isinstance (response .output_audio_tokens , int ) and response .output_audio_tokens >= 0
1389+ assert isinstance (response .cache_audio_read_tokens , int ) and response .cache_audio_read_tokens >= 0
1390+ assert isinstance (response .request_count , int ) and response .request_count >= 1
1391+ assert isinstance (response .tool_call_count , int ) and response .tool_call_count >= 0
1392+ assert isinstance (response .usage_details , dict )
1393+
1394+ # Print summary for debugging
1395+ print ("\n Usage Summary:" )
1396+ print (f" input_tokens: { response .input_tokens } " )
1397+ print (f" output_tokens: { response .output_tokens } " )
1398+ print (f" cache_read_tokens: { response .cache_read_tokens } " )
1399+ print (f" cache_write_tokens: { response .cache_write_tokens } " )
1400+ print (f" request_count: { response .request_count } " )
1401+ print (f" tool_call_count: { response .tool_call_count } " )
1402+ print (f" cost_microcents: { response .cost_microcents } " )
1403+ print (f" cost_dollars: ${ response .cost_dollars :.6f} " )
0 commit comments