Skip to content

Commit fcf67c2

Browse files
committed
new tests
1 parent 2f3a91f commit fcf67c2

3 files changed

Lines changed: 442 additions & 0 deletions

File tree

tests/test_agent.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,17 @@ def mock_result(self) -> MagicMock:
282282
usage.output_tokens = 50
283283
usage.total_tokens = 150
284284
usage.model = "gpt-4o"
285+
# Cache tokens
286+
usage.cache_read_tokens = 0
287+
usage.cache_write_tokens = 0
288+
# Audio tokens
289+
usage.input_audio_tokens = 0
290+
usage.output_audio_tokens = 0
291+
usage.cache_audio_read_tokens = 0
292+
# Request/tool metrics
293+
usage.requests = 1
294+
usage.tool_calls = 0
295+
usage.details = {}
285296
result.usage.return_value = usage
286297

287298
result.new_messages.return_value = []
@@ -397,6 +408,17 @@ async def test_extracts_tool_calls(self) -> None:
397408
usage.output_tokens = 5
398409
usage.total_tokens = 15
399410
usage.model = "gpt-4o"
411+
# Cache tokens
412+
usage.cache_read_tokens = 0
413+
usage.cache_write_tokens = 0
414+
# Audio tokens
415+
usage.input_audio_tokens = 0
416+
usage.output_audio_tokens = 0
417+
usage.cache_audio_read_tokens = 0
418+
# Request/tool metrics
419+
usage.requests = 1
420+
usage.tool_calls = 0
421+
usage.details = {}
400422
result.usage.return_value = usage
401423

402424
with patch.object(agent._agent, "run", new_callable=AsyncMock) as mock_run:
@@ -421,6 +443,17 @@ async def test_handles_no_tool_calls(self) -> None:
421443
usage.output_tokens = 5
422444
usage.total_tokens = 15
423445
usage.model = "gpt-4o"
446+
# Cache tokens
447+
usage.cache_read_tokens = 0
448+
usage.cache_write_tokens = 0
449+
# Audio tokens
450+
usage.input_audio_tokens = 0
451+
usage.output_audio_tokens = 0
452+
usage.cache_audio_read_tokens = 0
453+
# Request/tool metrics
454+
usage.requests = 1
455+
usage.tool_calls = 0
456+
usage.details = {}
424457
result.usage.return_value = usage
425458

426459
with patch.object(agent._agent, "run", new_callable=AsyncMock) as mock_run:
@@ -479,6 +512,17 @@ async def test_run_stream_yields_chunks(self) -> None:
479512
usage.output_tokens = 5
480513
usage.total_tokens = 15
481514
usage.model = "gpt-4o"
515+
# Cache tokens
516+
usage.cache_read_tokens = 0
517+
usage.cache_write_tokens = 0
518+
# Audio tokens
519+
usage.input_audio_tokens = 0
520+
usage.output_audio_tokens = 0
521+
usage.cache_audio_read_tokens = 0
522+
# Request/tool metrics
523+
usage.requests = 1
524+
usage.tool_calls = 0
525+
usage.details = {}
482526
mock_stream.usage.return_value = usage
483527
mock_stream.get_output.return_value = "Hello world"
484528
mock_stream.new_messages.return_value = []
@@ -513,6 +557,17 @@ async def test_run_stream_final_chunk_has_usage(self) -> None:
513557
usage.output_tokens = 25
514558
usage.total_tokens = 75
515559
usage.model = "gpt-4o"
560+
# Cache tokens
561+
usage.cache_read_tokens = 0
562+
usage.cache_write_tokens = 0
563+
# Audio tokens
564+
usage.input_audio_tokens = 0
565+
usage.output_audio_tokens = 0
566+
usage.cache_audio_read_tokens = 0
567+
# Request/tool metrics
568+
usage.requests = 1
569+
usage.tool_calls = 0
570+
usage.details = {}
516571
mock_stream.usage.return_value = usage
517572
mock_stream.get_output.return_value = "Response"
518573
mock_stream.new_messages.return_value = []

tests/test_integration.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

148222
class 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"\nResponse 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("\nUsage 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

Comments
 (0)