diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2177e483a1b..f119689e6ec 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -239,6 +239,8 @@ graph LR 7. `DBSpendUpdateWriter.update_database()` queues spend increments to Redis 8. Background job `update_spend` flushes queued spend to PostgreSQL every 60s +Cost lookup falls back to the response model when the provider-reported name cannot be priced. Explicitly registered zero rates remain zero, and custom deployment pricing or an explicit `base_model` retain precedence + ### Data Access Layer (Models & Repositories) Database entities and the operations on them live in two packages at the root of `litellm/` so both the gateway (`proxy/`) and the SDK can use them without importing proxy internals: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89a1b544b82..0ed63a1e7f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,8 @@ Add your tests to the [`tests/unit/` directory](https://github.com/BerriAI/litel - **Only add mocked tests** - no real LLM API calls in this directory - For integration tests with real APIs, use the appropriate test directories +Tests that call `litellm.register_model` or change `litellm.model_cost` should use the `local_model_cost_map` fixture from `tests/unit/conftest.py`. It provides a fresh catalog and restores the previous map, so pricing and capability overrides cannot leak into later tests + ### File Naming Convention The `tests/unit/` directory follows the same structure as `litellm/`: diff --git a/tests/unit/test_cost_calculator.py b/tests/unit/test_cost_calculator.py index 27b25c1f98e..84f6dab9eff 100644 --- a/tests/unit/test_cost_calculator.py +++ b/tests/unit/test_cost_calculator.py @@ -6810,6 +6810,73 @@ def test_completion_cost_bills_base_when_gemini_serves_on_demand( assert cost == pytest.approx(100 * 0.001 + 50 * 0.002) +def test_an_unpriced_provider_slug_does_not_bill_the_turn_at_zero( + _local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch +) -> None: + response_model: Final = "anthropic/claude-test-42161-response" + reported_model: Final = "claude-test-42161-unmapped" + monkeypatch.delitem(litellm.model_cost, reported_model, raising=False) + monkeypatch.delitem(litellm.model_cost, f"anthropic/{reported_model}", raising=False) + monkeypatch.setitem( + litellm.model_cost, + response_model, + { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "litellm_provider": "anthropic", + "mode": "chat", + }, + ) + response: Final = ModelResponse( + model=response_model, + choices=[], + usage=Usage(prompt_tokens=38, completion_tokens=24, total_tokens=62), + ) + response._hidden_params["provider_response_model"] = reported_model + + cost: Final = completion_cost(completion_response=response, custom_llm_provider="anthropic") + + assert cost == pytest.approx(38 * 0.001 + 24 * 0.002) + + +@pytest.mark.parametrize("reported_rate", (0.0, 0.01)) +def test_a_priced_provider_slug_still_decides_the_rate( + _local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch, reported_rate: float +) -> None: + response_model: Final = "anthropic/claude-test-42161-response" + reported_model: Final = "claude-test-42161-reported" + monkeypatch.setitem( + litellm.model_cost, + response_model, + { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "litellm_provider": "anthropic", + "mode": "chat", + }, + ) + monkeypatch.setitem( + litellm.model_cost, + f"anthropic/{reported_model}", + { + "input_cost_per_token": reported_rate, + "output_cost_per_token": reported_rate * 2, + "litellm_provider": "anthropic", + "mode": "chat", + }, + ) + response: Final = ModelResponse( + model=response_model, + choices=[], + usage=Usage(prompt_tokens=38, completion_tokens=24, total_tokens=62), + ) + response._hidden_params["provider_response_model"] = reported_model + + cost: Final = completion_cost(completion_response=response, custom_llm_provider="anthropic") + + assert cost == pytest.approx(38 * reported_rate + 24 * reported_rate * 2) + + def test_custom_pricing_as_completion_cost_param(): from litellm import Choices, Message, ModelResponse from litellm.utils import Usage