|
9 | 9 | import litellm |
10 | 10 | from litellm.types.utils import ( |
11 | 11 | CompletionTokensDetailsWrapper, |
| 12 | + PromptTokensDetailsWrapper, |
12 | 13 | Usage, |
13 | 14 | ) |
14 | 15 |
|
15 | 16 | sys.path.insert( |
16 | 17 | 0, os.path.abspath("../../..") |
17 | 18 | ) # Adds the parent directory to the system path |
18 | 19 |
|
19 | | -from litellm.llms.xai.cost_calculator import cost_per_token |
| 20 | +from litellm.llms.xai.cost_calculator import cost_per_token, cost_per_web_search_request |
20 | 21 |
|
21 | 22 |
|
22 | 23 | class TestXAICostCalculator: |
@@ -320,3 +321,73 @@ def test_tiered_pricing_model_without_tiered_pricing(self): |
320 | 321 |
|
321 | 322 | assert math.isclose(prompt_cost, expected_prompt_cost, rel_tol=1e-10) |
322 | 323 | assert math.isclose(completion_cost, expected_completion_cost, rel_tol=1e-10) |
| 324 | + |
| 325 | + def test_web_search_cost_calculation(self): |
| 326 | + """Test web search cost calculation for X.AI models.""" |
| 327 | + # Test with web_search_requests in prompt_tokens_details (primary path) |
| 328 | + usage = Usage( |
| 329 | + prompt_tokens=100, |
| 330 | + completion_tokens=50, |
| 331 | + total_tokens=150, |
| 332 | + prompt_tokens_details=PromptTokensDetailsWrapper( |
| 333 | + text_tokens=100, |
| 334 | + web_search_requests=3, # 3 sources used |
| 335 | + ) |
| 336 | + ) |
| 337 | + |
| 338 | + web_search_cost = cost_per_web_search_request(usage=usage, model_info={}) |
| 339 | + |
| 340 | + # Expected cost: 3 sources * $0.025 per source = $0.075 |
| 341 | + expected_cost = 3 * (25.0 / 1000.0) # 3 * $0.025 |
| 342 | + |
| 343 | + assert math.isclose(web_search_cost, expected_cost, rel_tol=1e-10) |
| 344 | + assert math.isclose(web_search_cost, 0.075, rel_tol=1e-10) |
| 345 | + |
| 346 | + def test_web_search_cost_fallback_calculation(self): |
| 347 | + """Test web search cost calculation using fallback num_sources_used.""" |
| 348 | + # Test fallback: num_sources_used on usage object |
| 349 | + usage = Usage( |
| 350 | + prompt_tokens=100, |
| 351 | + completion_tokens=50, |
| 352 | + total_tokens=150, |
| 353 | + ) |
| 354 | + # Manually set num_sources_used (as done by transformation layer) |
| 355 | + setattr(usage, "num_sources_used", 5) |
| 356 | + |
| 357 | + web_search_cost = cost_per_web_search_request(usage=usage, model_info={}) |
| 358 | + |
| 359 | + # Expected cost: 5 sources * $0.025 per source = $0.125 |
| 360 | + expected_cost = 5 * (25.0 / 1000.0) # 5 * $0.025 |
| 361 | + |
| 362 | + assert math.isclose(web_search_cost, expected_cost, rel_tol=1e-10) |
| 363 | + assert math.isclose(web_search_cost, 0.125, rel_tol=1e-10) |
| 364 | + |
| 365 | + def test_web_search_no_sources_used(self): |
| 366 | + """Test web search cost calculation when no sources are used.""" |
| 367 | + usage = Usage( |
| 368 | + prompt_tokens=100, |
| 369 | + completion_tokens=50, |
| 370 | + total_tokens=150, |
| 371 | + prompt_tokens_details=PromptTokensDetailsWrapper( |
| 372 | + text_tokens=100, |
| 373 | + web_search_requests=0, # No web search |
| 374 | + ) |
| 375 | + ) |
| 376 | + |
| 377 | + web_search_cost = cost_per_web_search_request(usage=usage, model_info={}) |
| 378 | + |
| 379 | + # Expected cost: 0 sources * $0.025 per source = $0.0 |
| 380 | + assert web_search_cost == 0.0 |
| 381 | + |
| 382 | + def test_web_search_cost_without_prompt_tokens_details(self): |
| 383 | + """Test web search cost calculation when prompt_tokens_details is None.""" |
| 384 | + usage = Usage( |
| 385 | + prompt_tokens=100, |
| 386 | + completion_tokens=50, |
| 387 | + total_tokens=150, |
| 388 | + ) |
| 389 | + |
| 390 | + web_search_cost = cost_per_web_search_request(usage=usage, model_info={}) |
| 391 | + |
| 392 | + # Expected cost: No web search data = $0.0 |
| 393 | + assert web_search_cost == 0.0 |
0 commit comments