mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(vertex_ai): bill Lyria per generation, not per audio second
Google prices Lyria per generated clip, so every Vertex Lyria entry in the price map now carries a single output_cost_per_image and both the speech and the passthrough cost paths read that one field. The old output_cost_per_second and audio_seconds_per_prediction pair assumed a 30 second clip, which does not match the 32.768 second WAV Vertex returns, and no other model in the map priced audio that way Drops max_audio_length_hours and max_audio_per_prompt from the price map, its schema, the generator, and ModelInfo, since nothing reads them, and drops the audio_mime_type hidden param for the same reason: the response already carries the resolved content type on its own header Folds the per-model bundled catalog lookups into one cached parse of the local cost map, validated with a TypeAdapter over a ReadOnly TypedDict
This commit is contained in:
parent
fd24cce2c3
commit
6be78fa850
18 changed files with 141 additions and 192 deletions
|
|
@ -121,10 +121,6 @@ ARRAY_KEYS: dict[str, JsonSchema] = {
|
|||
}
|
||||
|
||||
INTEGER_KEYS: dict[str, JsonSchema] = {
|
||||
"max_audio_per_prompt": {
|
||||
**NONNEG_INTEGER,
|
||||
"description": "Maximum number of audio outputs accepted or generated per prompt.",
|
||||
},
|
||||
"max_tokens": {
|
||||
**NONNEG_INTEGER,
|
||||
"description": "Legacy field: max output tokens if the provider specifies it, else max input tokens.",
|
||||
|
|
@ -150,14 +146,6 @@ INTEGER_KEYS: dict[str, JsonSchema] = {
|
|||
}
|
||||
|
||||
NUMBER_KEYS: dict[str, JsonSchema] = {
|
||||
"audio_seconds_per_prediction": {
|
||||
**NONNEG_NUMBER,
|
||||
"description": "Audio duration, in seconds, produced by one prediction.",
|
||||
},
|
||||
"max_audio_length_hours": {
|
||||
**NONNEG_NUMBER,
|
||||
"description": "Maximum generated audio duration, expressed in hours.",
|
||||
},
|
||||
"regional_processing_uplift_multiplier_eu": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ from litellm.llms.together_ai.cost_calculator import (
|
|||
get_model_params_and_category,
|
||||
has_together_registry_pricing,
|
||||
)
|
||||
from litellm.llms.vertex_ai.common_utils import get_vertex_ai_lyria_generation_cost
|
||||
from litellm.llms.vertex_ai.cost_calculator import (
|
||||
cost_per_character as google_cost_per_character,
|
||||
)
|
||||
|
|
@ -495,16 +496,17 @@ def cost_per_token(
|
|||
|
||||
# see this https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models
|
||||
if call_type == "speech" or call_type == "aspeech":
|
||||
if custom_llm_provider in ("vertex_ai", "vertex_ai_beta"):
|
||||
from litellm.llms.vertex_ai.common_utils import get_vertex_ai_lyria_generation_cost
|
||||
|
||||
lyria_generation_cost: Final = get_vertex_ai_lyria_generation_cost(model_without_prefix)
|
||||
if lyria_generation_cost is not None:
|
||||
return 0.0, lyria_generation_cost
|
||||
lyria_generation_cost: Final = (
|
||||
get_vertex_ai_lyria_generation_cost(model=model_without_prefix)
|
||||
if custom_llm_provider in ("vertex_ai", "vertex_ai_beta")
|
||||
else None
|
||||
)
|
||||
if lyria_generation_cost is not None:
|
||||
return 0.0, lyria_generation_cost
|
||||
speech_model_info = litellm.get_model_info(model=model_without_prefix, custom_llm_provider=custom_llm_provider)
|
||||
cost_metric: Final = select_cost_metric_for_model(speech_model_info)
|
||||
prompt_cost: float = 0.0
|
||||
completion_cost: float = 0.0
|
||||
cost_metric: Final = select_cost_metric_for_model(speech_model_info)
|
||||
if cost_metric == "cost_per_character":
|
||||
if prompt_characters is None:
|
||||
raise ValueError(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
|
|
@ -29,8 +30,6 @@ class VertexAILyriaModelInfo(TypedDict):
|
|||
vertex_ai_audio_api: ReadOnly[Literal["lyria_predict", "lyria_interactions"]]
|
||||
supported_audio_formats: ReadOnly[tuple[Literal["mp3", "wav"], ...]]
|
||||
output_cost_per_image: NotRequired[ReadOnly[float]]
|
||||
output_cost_per_second: NotRequired[ReadOnly[float]]
|
||||
audio_seconds_per_prediction: NotRequired[ReadOnly[float]]
|
||||
|
||||
|
||||
_VERTEX_AI_LYRIA_MODEL_INFO_ADAPTER: Final = TypeAdapter(VertexAILyriaModelInfo)
|
||||
|
|
@ -45,37 +44,41 @@ def _validate_vertex_ai_lyria_model_info(raw_model_info: object) -> VertexAILyri
|
|||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _get_bundled_vertex_ai_lyria_model_info(model_key: str) -> VertexAILyriaModelInfo | None:
|
||||
@lru_cache(maxsize=1)
|
||||
def _bundled_vertex_ai_lyria_model_infos() -> Mapping[str, VertexAILyriaModelInfo]:
|
||||
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
|
||||
|
||||
bundled_model_info: Final = GetModelCostMap.load_local_model_cost_map().get(model_key)
|
||||
return _validate_vertex_ai_lyria_model_info(bundled_model_info)
|
||||
return MappingProxyType(
|
||||
{
|
||||
model_key: lyria_model_info
|
||||
for model_key, raw_model_info in GetModelCostMap.load_local_model_cost_map().items()
|
||||
if (lyria_model_info := _validate_vertex_ai_lyria_model_info(raw_model_info)) is not None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _vertex_ai_lyria_model_key(model: str) -> str:
|
||||
return model if model.startswith("vertex_ai/") else f"vertex_ai/{model}"
|
||||
|
||||
|
||||
def _vertex_ai_lyria_generation_cost(model_info: VertexAILyriaModelInfo | None) -> float | None:
|
||||
return None if model_info is None else model_info.get("output_cost_per_image")
|
||||
|
||||
|
||||
def get_vertex_ai_lyria_model_info(model: str) -> VertexAILyriaModelInfo | None:
|
||||
model_key: Final = model if model.startswith("vertex_ai/") else f"vertex_ai/{model}"
|
||||
model_key: Final = _vertex_ai_lyria_model_key(model)
|
||||
runtime_model_info: Final = _validate_vertex_ai_lyria_model_info(litellm.model_cost.get(model_key))
|
||||
bundled_model_info: Final = _get_bundled_vertex_ai_lyria_model_info(model_key)
|
||||
if runtime_model_info is None:
|
||||
return bundled_model_info
|
||||
if bundled_model_info is None:
|
||||
return runtime_model_info
|
||||
return _validate_vertex_ai_lyria_model_info(MappingProxyType({**bundled_model_info, **runtime_model_info}))
|
||||
return runtime_model_info or _bundled_vertex_ai_lyria_model_infos().get(model_key)
|
||||
|
||||
|
||||
def get_vertex_ai_lyria_generation_cost(model: str) -> float | None:
|
||||
model_info: Final = get_vertex_ai_lyria_model_info(model)
|
||||
if model_info is None:
|
||||
return None
|
||||
generation_cost: Final = model_info.get("output_cost_per_image")
|
||||
if generation_cost is not None:
|
||||
return generation_cost
|
||||
cost_per_second: Final = model_info.get("output_cost_per_second")
|
||||
seconds_per_prediction: Final = model_info.get("audio_seconds_per_prediction")
|
||||
if cost_per_second is None or seconds_per_prediction is None:
|
||||
return None
|
||||
return cost_per_second * seconds_per_prediction
|
||||
model_key: Final = _vertex_ai_lyria_model_key(model)
|
||||
runtime_cost: Final = _vertex_ai_lyria_generation_cost(
|
||||
_validate_vertex_ai_lyria_model_info(litellm.model_cost.get(model_key))
|
||||
)
|
||||
if runtime_cost is not None:
|
||||
return runtime_cost
|
||||
return _vertex_ai_lyria_generation_cost(_bundled_vertex_ai_lyria_model_infos().get(model_key))
|
||||
|
||||
|
||||
class VertexAIError(BaseLLMException):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import httpx
|
|||
import litellm
|
||||
from litellm.exceptions import UnsupportedParamsError
|
||||
from litellm.litellm_core_utils.audio_utils.utils import (
|
||||
DEFAULT_SPEECH_MEDIA_TYPE,
|
||||
speech_media_type_from_audio_bytes,
|
||||
)
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
|
|
@ -571,13 +572,11 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig):
|
|||
VertexAIInteractionsConfig,
|
||||
)
|
||||
|
||||
resolved_project: Final = project
|
||||
|
||||
def mint_access_token(
|
||||
_credentials: VERTEX_CREDENTIALS_TYPES | None,
|
||||
project_id: str | None,
|
||||
) -> tuple[str, str]:
|
||||
return "", project_id or resolved_project
|
||||
return "", project_id or project
|
||||
|
||||
return VertexAIInteractionsConfig(mint_access_token=mint_access_token).get_complete_url(
|
||||
api_base=api_base,
|
||||
|
|
@ -681,24 +680,12 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig):
|
|||
) # rebind-ok: interactions response supplies its audio MIME type
|
||||
if audio_data is None:
|
||||
raise ValueError(f"No generated audio found in Vertex AI {base_model} response")
|
||||
decoded_audio: Final = base64.b64decode(audio_data)
|
||||
default_format: Final = model_info["supported_audio_formats"][0]
|
||||
mime_type = (
|
||||
mime_type
|
||||
or speech_media_type_from_audio_bytes(decoded_audio)
|
||||
or { # mutable-ok: short-lived lookup selects the default response MIME type; rebind-ok: absent provider MIME type falls back to model metadata
|
||||
"mp3": "audio/mpeg",
|
||||
"wav": "audio/wav",
|
||||
}[default_format]
|
||||
)
|
||||
response: Final = HttpxBinaryResponseContent(
|
||||
binary_data: Final = base64.b64decode(audio_data)
|
||||
media_type: Final = mime_type or speech_media_type_from_audio_bytes(binary_data) or DEFAULT_SPEECH_MEDIA_TYPE
|
||||
return HttpxBinaryResponseContent(
|
||||
httpx.Response(
|
||||
status_code=raw_response.status_code,
|
||||
content=decoded_audio,
|
||||
headers={ # mutable-ok: httpx requires a concrete response header dictionary
|
||||
"content-type": mime_type
|
||||
},
|
||||
content=binary_data,
|
||||
headers=MappingProxyType({"content-type": media_type}),
|
||||
)
|
||||
)
|
||||
response.set_audio_mime_type(mime_type)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -8260,14 +8260,11 @@ def speech(
|
|||
|
||||
# Vertex AI Text-to-Speech (Google Cloud TTS)
|
||||
if text_to_speech_provider_config is None:
|
||||
if VertexAILyriaTextToSpeechConfig.is_lyria_model(model):
|
||||
text_to_speech_provider_config = (
|
||||
VertexAILyriaTextToSpeechConfig()
|
||||
) # rebind-ok: model metadata selects the Lyria provider implementation
|
||||
else:
|
||||
text_to_speech_provider_config = (
|
||||
VertexAITextToSpeechConfig()
|
||||
) # rebind-ok: non-Lyria Vertex models use the standard TTS implementation
|
||||
text_to_speech_provider_config = ( # rebind-ok: model metadata selects the Vertex TTS implementation
|
||||
VertexAILyriaTextToSpeechConfig()
|
||||
if VertexAILyriaTextToSpeechConfig.is_lyria_model(model)
|
||||
else VertexAITextToSpeechConfig()
|
||||
)
|
||||
|
||||
# Cast to specific Vertex AI config type to access dispatch method
|
||||
vertex_config: Final = cast(VertexAITextToSpeechConfig, text_to_speech_provider_config)
|
||||
|
|
|
|||
|
|
@ -45512,12 +45512,9 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"vertex_ai/lyria-002": {
|
||||
"audio_seconds_per_prediction": 30,
|
||||
"litellm_provider": "vertex_ai",
|
||||
"max_audio_length_hours": 0.009111111111111111,
|
||||
"max_audio_per_prompt": 4,
|
||||
"mode": "audio_speech",
|
||||
"output_cost_per_second": 0.002,
|
||||
"mode": "audio_speech",
|
||||
"output_cost_per_image": 0.06,
|
||||
"source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing#lyria",
|
||||
"supported_audio_formats": [
|
||||
"wav"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
from urllib.parse import urlparse
|
||||
|
|
@ -346,9 +345,7 @@ class VertexPassthroughLoggingHandler:
|
|||
prediction_count: Final = VertexPassthroughLoggingHandler._get_audio_prediction_count(
|
||||
json_response=json_response
|
||||
)
|
||||
response_cost: Final = (
|
||||
VertexPassthroughLoggingHandler._get_audio_prediction_unit_cost(model=model) or 0.0
|
||||
) * prediction_count
|
||||
response_cost: Final = (get_vertex_ai_lyria_generation_cost(model=model) or 0.0) * prediction_count
|
||||
|
||||
logging_obj.model = model # rebind-ok: passthrough attribution records the resolved Vertex model
|
||||
logging_obj.model_call_details[ # rebind-ok: passthrough attribution enriches callback metadata
|
||||
|
|
@ -387,30 +384,9 @@ class VertexPassthroughLoggingHandler:
|
|||
) -> bool:
|
||||
return (
|
||||
VertexPassthroughLoggingHandler._get_audio_prediction_count(json_response=json_response) > 0
|
||||
and VertexPassthroughLoggingHandler._get_audio_prediction_unit_cost(model=model) is not None
|
||||
and get_vertex_ai_lyria_generation_cost(model=model) is not None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_audio_prediction_unit_cost(model: str) -> float | None:
|
||||
runtime_unit_cost: Final = VertexPassthroughLoggingHandler._audio_prediction_unit_cost_from_model_info(
|
||||
model_info=litellm.model_cost.get(f"vertex_ai/{model}")
|
||||
)
|
||||
if runtime_unit_cost is not None:
|
||||
return runtime_unit_cost
|
||||
return get_vertex_ai_lyria_generation_cost(model=model)
|
||||
|
||||
@staticmethod
|
||||
def _audio_prediction_unit_cost_from_model_info(model_info: object) -> float | None:
|
||||
if not isinstance(model_info, Mapping):
|
||||
return None
|
||||
output_cost_per_second: Final = model_info.get("output_cost_per_second")
|
||||
audio_seconds_per_prediction: Final = model_info.get("audio_seconds_per_prediction")
|
||||
if not isinstance(output_cost_per_second, (int, float)) or not isinstance(
|
||||
audio_seconds_per_prediction, (int, float)
|
||||
):
|
||||
return None
|
||||
return float(output_cost_per_second * audio_seconds_per_prediction)
|
||||
|
||||
@staticmethod
|
||||
def _get_audio_prediction_count(
|
||||
json_response: dict, # mutable-ok: counter inspects the decoded provider response dictionary without mutation
|
||||
|
|
|
|||
|
|
@ -11176,14 +11176,9 @@ async def audio_speech(
|
|||
upstream_content_type: Final = (
|
||||
response.response.headers.get("content-type") if isinstance(response, HttpxBinaryResponseContent) else None
|
||||
)
|
||||
hidden_audio_mime_type: Final = hidden_params.get("audio_mime_type")
|
||||
media_type: Final = (
|
||||
hidden_audio_mime_type
|
||||
if isinstance(hidden_audio_mime_type, str)
|
||||
else resolve_speech_media_type(
|
||||
upstream_content_type=upstream_content_type,
|
||||
response_format=requested_format if isinstance(requested_format, str) else None,
|
||||
)
|
||||
media_type: Final = resolve_speech_media_type(
|
||||
upstream_content_type=upstream_content_type,
|
||||
response_format=requested_format if isinstance(requested_format, str) else None,
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
|
|
|
|||
|
|
@ -119,9 +119,6 @@ class HttpxBinaryResponseContent(_HttpxBinaryResponseContent):
|
|||
return
|
||||
self._hidden_params["response_cost"] = response_cost
|
||||
|
||||
def set_audio_mime_type(self, audio_mime_type: str) -> None:
|
||||
self._hidden_params["audio_mime_type"] = audio_mime_type
|
||||
|
||||
|
||||
class NotGiven:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -312,9 +312,6 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
|
|||
output_cost_per_video_per_second: float | None # only for vertex ai models
|
||||
output_cost_per_audio_per_second: float | None # only for vertex ai models
|
||||
output_cost_per_second: float | None # for OpenAI Speech models
|
||||
audio_seconds_per_prediction: ReadOnly[float | None]
|
||||
max_audio_length_hours: ReadOnly[float | None]
|
||||
max_audio_per_prompt: ReadOnly[int | None]
|
||||
output_cost_per_second_1080p: (
|
||||
float | None
|
||||
) # video_generation tier: key output_cost_per_second_<resolution> (e.g. 1080p, 720p)
|
||||
|
|
|
|||
|
|
@ -5880,9 +5880,6 @@ def _get_model_info_helper(
|
|||
"output_cost_per_token_above_512k_tokens", None
|
||||
),
|
||||
output_cost_per_second=_model_info.get("output_cost_per_second", None),
|
||||
audio_seconds_per_prediction=_model_info.get("audio_seconds_per_prediction", None),
|
||||
max_audio_length_hours=_model_info.get("max_audio_length_hours", None),
|
||||
max_audio_per_prompt=_model_info.get("max_audio_per_prompt", None),
|
||||
output_cost_per_second_1080p=_model_info.get("output_cost_per_second_1080p", None),
|
||||
output_cost_per_second_480p=_model_info.get("output_cost_per_second_480p", None),
|
||||
output_cost_per_second_4k=_model_info.get("output_cost_per_second_4k", None),
|
||||
|
|
|
|||
|
|
@ -45512,12 +45512,9 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"vertex_ai/lyria-002": {
|
||||
"audio_seconds_per_prediction": 30,
|
||||
"litellm_provider": "vertex_ai",
|
||||
"max_audio_length_hours": 0.009111111111111111,
|
||||
"max_audio_per_prompt": 4,
|
||||
"mode": "audio_speech",
|
||||
"output_cost_per_second": 0.002,
|
||||
"mode": "audio_speech",
|
||||
"output_cost_per_image": 0.06,
|
||||
"source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing#lyria",
|
||||
"supported_audio_formats": [
|
||||
"wav"
|
||||
|
|
|
|||
|
|
@ -53,11 +53,6 @@
|
|||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"audio_seconds_per_prediction": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Audio duration, in seconds, produced by one prediction."
|
||||
},
|
||||
"audio_transcription_config": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -368,16 +363,6 @@
|
|||
"type": "string",
|
||||
"description": "LiteLLM provider slug; one of https://docs.litellm.ai/docs/providers."
|
||||
},
|
||||
"max_audio_length_hours": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Maximum generated audio duration, expressed in hours."
|
||||
},
|
||||
"max_audio_per_prompt": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Maximum number of audio outputs accepted or generated per prompt."
|
||||
},
|
||||
"max_input_tokens": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
|
|
|
|||
|
|
@ -1696,3 +1696,44 @@ def test_vertex_text_embedding_request_includes_labels_from_metadata():
|
|||
},
|
||||
)
|
||||
assert req.get("labels") == {"project_id": "cost-center-1"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "expected_api"),
|
||||
[
|
||||
("lyria-002", "lyria_predict"),
|
||||
("vertex_ai/lyria-002", "lyria_predict"),
|
||||
("lyria-3-clip-preview", "lyria_interactions"),
|
||||
("lyria-3-pro-preview", "lyria_interactions"),
|
||||
],
|
||||
)
|
||||
def test_get_vertex_ai_lyria_model_info_resolves_audio_api(model, expected_api):
|
||||
from litellm.llms.vertex_ai.common_utils import get_vertex_ai_lyria_model_info
|
||||
|
||||
model_info = get_vertex_ai_lyria_model_info(model=model)
|
||||
|
||||
assert model_info is not None
|
||||
assert model_info["vertex_ai_audio_api"] == expected_api
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["en-US-Studio-O", "gemini-2.5-flash-preview-tts", "chirp-3-hd-charon"])
|
||||
def test_get_vertex_ai_lyria_model_info_is_none_for_non_lyria_speech_models(model):
|
||||
from litellm.llms.vertex_ai.common_utils import get_vertex_ai_lyria_model_info
|
||||
|
||||
assert get_vertex_ai_lyria_model_info(model=model) is None
|
||||
|
||||
|
||||
def test_get_vertex_ai_lyria_model_info_falls_back_to_bundled_map(monkeypatch):
|
||||
import litellm
|
||||
from litellm.llms.vertex_ai.common_utils import get_vertex_ai_lyria_model_info
|
||||
|
||||
stale_runtime_model_cost = {
|
||||
key: value for key, value in litellm.model_cost.items() if not key.startswith("vertex_ai/lyria")
|
||||
}
|
||||
monkeypatch.setattr(litellm, "model_cost", stale_runtime_model_cost)
|
||||
|
||||
model_info = get_vertex_ai_lyria_model_info(model="lyria-3-pro-preview")
|
||||
|
||||
assert model_info is not None
|
||||
assert model_info["vertex_ai_audio_api"] == "lyria_interactions"
|
||||
assert model_info["supported_audio_formats"] == ("mp3", "wav")
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ def test_lyria_predict_response_preserves_audio_response_and_logs_cost(
|
|||
litellm.model_cost,
|
||||
"vertex_ai/lyria-002",
|
||||
{
|
||||
"audio_seconds_per_prediction": 30,
|
||||
"output_cost_per_second": 0.002,
|
||||
"vertex_ai_audio_api": "lyria_predict",
|
||||
"supported_audio_formats": ["wav"],
|
||||
"output_cost_per_image": 0.06,
|
||||
},
|
||||
)
|
||||
logging_obj = MagicMock()
|
||||
|
|
@ -79,8 +80,9 @@ def test_audio_predict_response_uses_model_map_metadata(
|
|||
litellm.model_cost,
|
||||
"vertex_ai/music-audio-preview",
|
||||
{
|
||||
"audio_seconds_per_prediction": 12,
|
||||
"output_cost_per_second": 0.5,
|
||||
"vertex_ai_audio_api": "lyria_predict",
|
||||
"supported_audio_formats": ["wav"],
|
||||
"output_cost_per_image": 0.5,
|
||||
},
|
||||
)
|
||||
logging_obj = MagicMock()
|
||||
|
|
@ -109,8 +111,8 @@ def test_audio_predict_response_uses_model_map_metadata(
|
|||
)
|
||||
|
||||
assert result["kwargs"]["model"] == "music-audio-preview"
|
||||
assert result["kwargs"]["response_cost"] == pytest.approx(6.0)
|
||||
assert logging_obj.model_call_details["response_cost"] == pytest.approx(6.0)
|
||||
assert result["kwargs"]["response_cost"] == pytest.approx(0.5)
|
||||
assert logging_obj.model_call_details["response_cost"] == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_audio_predict_response_supports_bytes_base64_encoded(
|
||||
|
|
@ -120,8 +122,9 @@ def test_audio_predict_response_supports_bytes_base64_encoded(
|
|||
litellm.model_cost,
|
||||
"vertex_ai/lyria-002",
|
||||
{
|
||||
"audio_seconds_per_prediction": 30,
|
||||
"output_cost_per_second": 0.002,
|
||||
"vertex_ai_audio_api": "lyria_predict",
|
||||
"supported_audio_formats": ["wav"],
|
||||
"output_cost_per_image": 0.06,
|
||||
},
|
||||
)
|
||||
logging_obj = MagicMock()
|
||||
|
|
@ -146,27 +149,23 @@ def test_audio_predict_response_supports_bytes_base64_encoded(
|
|||
assert logging_obj.model_call_details["response_cost"] == pytest.approx(0.06)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"missing_fields",
|
||||
(
|
||||
None,
|
||||
("output_cost_per_second",),
|
||||
("audio_seconds_per_prediction",),
|
||||
("output_cost_per_second", "audio_seconds_per_prediction"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("runtime_entry_is_missing", (True, False))
|
||||
def test_lyria_predict_cost_falls_back_to_bundled_map_when_runtime_metadata_is_incomplete(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
missing_fields: tuple[str, ...] | None,
|
||||
runtime_entry_is_missing: bool,
|
||||
local_model_cost_map: None,
|
||||
) -> None:
|
||||
if missing_fields is None:
|
||||
if runtime_entry_is_missing:
|
||||
monkeypatch.delitem(litellm.model_cost, "vertex_ai/lyria-002")
|
||||
else:
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
"vertex_ai/lyria-002",
|
||||
{key: value for key, value in litellm.model_cost["vertex_ai/lyria-002"].items() if key not in missing_fields},
|
||||
{
|
||||
key: value
|
||||
for key, value in litellm.model_cost["vertex_ai/lyria-002"].items()
|
||||
if key != "output_cost_per_image"
|
||||
},
|
||||
)
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {}
|
||||
|
|
@ -193,7 +192,7 @@ def test_lyria_predict_cost_falls_back_to_bundled_map_when_runtime_metadata_is_i
|
|||
request_body={"instances": [{"prompt": "ambient piano"}]},
|
||||
)
|
||||
|
||||
if missing_fields is None:
|
||||
if runtime_entry_is_missing:
|
||||
assert "vertex_ai/lyria-002" not in litellm.model_cost
|
||||
assert result["kwargs"]["model"] == "lyria-002"
|
||||
assert result["kwargs"]["response_cost"] == pytest.approx(0.06)
|
||||
|
|
|
|||
|
|
@ -169,23 +169,6 @@ def test_transform_text_to_speech_response_leaves_unknown_bytes_unlabeled():
|
|||
|
||||
|
||||
class TestVertexAILyriaTextToSpeechConfig:
|
||||
def test_response_without_mime_type_uses_audio_container(self) -> None:
|
||||
audio: Final = b"RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00"
|
||||
raw_response: Final = httpx.Response(
|
||||
200,
|
||||
json={"outputs": [{"type": "audio", "data": base64.b64encode(audio).decode()}]},
|
||||
)
|
||||
|
||||
response: Final = VertexAILyriaTextToSpeechConfig().transform_text_to_speech_response(
|
||||
model="lyria-3-pro-preview",
|
||||
raw_response=raw_response,
|
||||
logging_obj=MagicMock(),
|
||||
)
|
||||
|
||||
assert response.content == audio
|
||||
assert response.response.headers["content-type"] == "audio/wav"
|
||||
assert response._hidden_params["audio_mime_type"] == "audio/wav"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
["lyria-002", "vertex_ai/lyria-3-clip-preview", "lyria-3-pro-preview"],
|
||||
|
|
@ -385,11 +368,11 @@ class TestVertexAILyriaTextToSpeechConfig:
|
|||
{
|
||||
"predictions": [
|
||||
{
|
||||
"bytesBase64Encoded": "bHlyaWEtMi1hdWRpbw==",
|
||||
"bytesBase64Encoded": "UklGRiQAAABXQVZFZm10IA==",
|
||||
}
|
||||
]
|
||||
},
|
||||
b"lyria-2-audio",
|
||||
b"RIFF$\x00\x00\x00WAVEfmt ",
|
||||
"audio/wav",
|
||||
),
|
||||
(
|
||||
|
|
@ -427,6 +410,19 @@ class TestVertexAILyriaTextToSpeechConfig:
|
|||
b"lyria-3-audio",
|
||||
"audio/mpeg",
|
||||
),
|
||||
(
|
||||
"lyria-3-pro-preview",
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"type": "audio",
|
||||
"data": "UklGRiQAAABXQVZFZm10IA==",
|
||||
}
|
||||
]
|
||||
},
|
||||
b"RIFF$\x00\x00\x00WAVEfmt ",
|
||||
"audio/wav",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_transform_response(
|
||||
|
|
@ -446,7 +442,7 @@ class TestVertexAILyriaTextToSpeechConfig:
|
|||
)
|
||||
|
||||
assert response.content == expected_audio
|
||||
assert response._hidden_params["audio_mime_type"] == expected_mime_type
|
||||
assert response.response.headers["content-type"] == expected_mime_type
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "response_format"),
|
||||
|
|
|
|||
|
|
@ -172,16 +172,15 @@ def test_vertex_lyria_speech_cost(
|
|||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
model,
|
||||
{
|
||||
key: value
|
||||
for key, value in model_info.items()
|
||||
if key not in ("output_cost_per_image", "output_cost_per_second", "audio_seconds_per_prediction")
|
||||
},
|
||||
{key: value for key, value in model_info.items() if key != "output_cost_per_image"},
|
||||
)
|
||||
elif runtime_state in ("custom_zero", "custom_price"):
|
||||
cost_key: Final = "output_cost_per_image" if "output_cost_per_image" in model_info else "output_cost_per_second"
|
||||
multiplier: Final = 0 if runtime_state == "custom_zero" else 2
|
||||
monkeypatch.setitem(litellm.model_cost, model, {**model_info, cost_key: model_info[cost_key] * multiplier})
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
model,
|
||||
{**model_info, "output_cost_per_image": model_info["output_cost_per_image"] * multiplier},
|
||||
)
|
||||
|
||||
cost: Final = completion_cost(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -944,14 +944,11 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
|
|||
"code_interpreter_cost_per_session": {"type": "number"},
|
||||
"inference_geo": {"type": "string"},
|
||||
"litellm_provider": {"type": "string"},
|
||||
"max_audio_length_hours": {"type": "number"},
|
||||
"max_audio_per_prompt": {"type": "number"},
|
||||
"max_input_tokens": {"type": "number"},
|
||||
"max_output_tokens": {"type": "number"},
|
||||
"max_tokens": {"type": "number"},
|
||||
"metadata": {"type": "object"},
|
||||
"provider_specific_entry": {"type": "object"},
|
||||
"audio_seconds_per_prediction": {"type": "number"},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
@ -2869,8 +2866,7 @@ def test_vertex_ai_lyria_models_in_cost_map():
|
|||
assert lyria_2["mode"] == "audio_speech"
|
||||
assert clip["mode"] == "audio_speech"
|
||||
assert pro["mode"] == "audio_speech"
|
||||
assert lyria_2["audio_seconds_per_prediction"] == 30
|
||||
assert lyria_2["output_cost_per_second"] == 0.002
|
||||
assert lyria_2["output_cost_per_image"] == 0.06
|
||||
assert lyria_2["supported_modalities"] == ["text"]
|
||||
assert lyria_2["supported_output_modalities"] == ["audio"]
|
||||
assert lyria_2["supports_audio_output"] is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue