diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 38f6acf9ba5..bd69b9e491c 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -1116,8 +1116,14 @@ def _calculate_input_cost( ) -> float: """ Calculates the input cost for a given model, prompt tokens, and completion tokens. + + A model priced per character (`input_cost_per_character`) bills the reported character count + in place of its text tokens, so the same prompt is never charged under both units. """ - prompt_cost = float(prompt_tokens_details["text_tokens"]) * prompt_base_cost + bills_characters: Final = ( + prompt_tokens_details["character_count"] > 0 and model_info.get("input_cost_per_character") is not None + ) + prompt_cost = 0.0 if bills_characters else float(prompt_tokens_details["text_tokens"]) * prompt_base_cost ### CACHE READ COST - Now uses tiered pricing cache_hit_audio_tokens: Final = prompt_tokens_details["cache_hit_audio_tokens"] diff --git a/litellm/llms/vertex_ai/vertex_embeddings/transformation.py b/litellm/llms/vertex_ai/vertex_embeddings/transformation.py index cab2bed8a60..ddc83bc53a5 100644 --- a/litellm/llms/vertex_ai/vertex_embeddings/transformation.py +++ b/litellm/llms/vertex_ai/vertex_embeddings/transformation.py @@ -1,13 +1,35 @@ import types from typing import Final, Literal +from pydantic import TypeAdapter +from typing_extensions import ReadOnly, TypedDict + from litellm.llms.vertex_ai.common_utils import pop_vertex_request_labels from litellm.types.llms.base import LiteLLMBaseModel -from litellm.types.utils import EmbeddingResponse, Usage +from litellm.types.utils import EmbeddingResponse, PromptTokensDetailsWrapper, Usage from .types import * +class _PredictMetadata(TypedDict, total=False): + billableCharacterCount: ReadOnly[int] + + +class _PredictResponseMetadata(TypedDict, total=False): + metadata: ReadOnly[_PredictMetadata] + + +_PREDICT_RESPONSE_METADATA: Final = TypeAdapter(_PredictResponseMetadata) + + +def billable_character_count(response: _PredictResponseMetadata) -> int | None: + """ + `metadata.billableCharacterCount` of a Vertex text embedding predict response, the count Google + bills per-character embedding models (text-embedding-005, text-multilingual-embedding-002) on. + """ + return response.get("metadata", {}).get("billableCharacterCount") + + class VertexAITextEmbeddingConfig(LiteLLMBaseModel): """ Reference: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api#TextEmbeddingInput @@ -247,7 +269,15 @@ class VertexAITextEmbeddingConfig(LiteLLMBaseModel): model_response.object = "list" model_response.data = embedding_response model_response.model = model - usage: Final = Usage(prompt_tokens=input_tokens, completion_tokens=0, total_tokens=input_tokens) + billable_characters: Final = billable_character_count(_PREDICT_RESPONSE_METADATA.validate_python(response)) + usage: Final = Usage( + prompt_tokens=input_tokens, + completion_tokens=0, + total_tokens=input_tokens, + prompt_tokens_details=( + None if billable_characters is None else PromptTokensDetailsWrapper(character_count=billable_characters) + ), + ) setattr(model_response, "usage", usage) return model_response diff --git a/tests/unit/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/unit/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index 01b20f9d0bd..be571d64b5b 100644 --- a/tests/unit/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/unit/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -99,6 +99,31 @@ def test_generic_cost_per_token_bills_cache_reads_at_input_rate_when_no_cache_re assert completion_cost == pytest.approx(380 * 9.7e-7) +@pytest.mark.parametrize( + ("model_info", "expected_prompt_cost"), + [ + ({"input_cost_per_token": 1e-7, "input_cost_per_character": 2.5e-8, "output_cost_per_token": 0.0}, 35 * 2.5e-8), + ({"input_cost_per_token": 1.5e-7, "output_cost_per_token": 0.0}, 11 * 1.5e-7), + ], + ids=["character_priced_model_bills_characters_instead_of_tokens", "token_priced_model_ignores_the_character_count"], +) +def test_generic_cost_per_token_bills_a_character_counted_prompt_under_one_unit( + model_info: ModelInfo, expected_prompt_cost: float +) -> None: + usage = Usage( + prompt_tokens=11, + completion_tokens=0, + total_tokens=11, + prompt_tokens_details=PromptTokensDetailsWrapper(character_count=35), + ) + + prompt_cost, completion_cost = generic_cost_per_token( + model="embedding-fixture", usage=usage, custom_llm_provider="vertex_ai", model_info=model_info + ) + + assert (prompt_cost, completion_cost) == (pytest.approx(expected_prompt_cost), 0.0) + + def test_apply_provider_cache_read_default_only_derives_a_rate_for_fireworks() -> None: openai_info: ModelInfo = {"input_cost_per_token": 2e-6} fireworks_info: ModelInfo = {"input_cost_per_token": 2e-6}