mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(audio): bill finalized streams and authorize transcription models
This commit is contained in:
parent
caea679de0
commit
8783a3b301
7 changed files with 73 additions and 16 deletions
|
|
@ -628,7 +628,25 @@ def cost_per_token(
|
|||
data_residency=data_residency,
|
||||
)
|
||||
elif call_type == "atranscription" or call_type == "transcription":
|
||||
if _transcription_usage_has_token_details(usage_block):
|
||||
transcription_model_info: Final = _cached_get_model_info_helper(
|
||||
model=model_without_prefix, custom_llm_provider=custom_llm_provider
|
||||
)
|
||||
has_token_pricing: Final = any(
|
||||
transcription_model_info.get(field)
|
||||
for field in (
|
||||
"input_cost_per_token",
|
||||
"output_cost_per_token",
|
||||
"input_cost_per_audio_token",
|
||||
"output_cost_per_audio_token",
|
||||
)
|
||||
)
|
||||
if _transcription_usage_has_token_details(usage_block) and (
|
||||
has_token_pricing
|
||||
or (
|
||||
transcription_model_info.get("input_cost_per_second") is None
|
||||
and transcription_model_info.get("output_cost_per_second") is None
|
||||
)
|
||||
):
|
||||
return generic_cost_per_token(
|
||||
model=model_without_prefix,
|
||||
usage=usage_block,
|
||||
|
|
|
|||
|
|
@ -3286,7 +3286,12 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
if "async_complete_streaming_response" in self.model_call_details:
|
||||
return # break out of this.
|
||||
complete_streaming_response: Final[
|
||||
ModelResponse | TextCompletionResponse | ResponsesAPIResponse | InteractionsAPIResponse | None
|
||||
ModelResponse
|
||||
| TextCompletionResponse
|
||||
| ResponsesAPIResponse
|
||||
| InteractionsAPIResponse
|
||||
| TranscriptionResponse
|
||||
| None
|
||||
] = self._get_assembled_streaming_response(
|
||||
result=result,
|
||||
start_time=start_time,
|
||||
|
|
@ -4105,10 +4110,17 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
end_time: datetime.datetime,
|
||||
is_async: bool,
|
||||
streaming_chunks: list[object],
|
||||
) -> ModelResponse | TextCompletionResponse | ResponsesAPIResponse | InteractionsAPIResponse | None:
|
||||
) -> (
|
||||
ModelResponse
|
||||
| TextCompletionResponse
|
||||
| ResponsesAPIResponse
|
||||
| InteractionsAPIResponse
|
||||
| TranscriptionResponse
|
||||
| None
|
||||
):
|
||||
if self.stream is not True:
|
||||
return None
|
||||
if isinstance(result, ModelResponse) or isinstance(result, TextCompletionResponse):
|
||||
if isinstance(result, (ModelResponse, TextCompletionResponse, TranscriptionResponse)):
|
||||
return result
|
||||
elif isinstance(
|
||||
result,
|
||||
|
|
|
|||
|
|
@ -31,9 +31,13 @@ class TranscriptionUsageObjectTransformation:
|
|||
prompt_tokens=usage_object.input_tokens,
|
||||
completion_tokens=usage_object.output_tokens,
|
||||
total_tokens=usage_object.total_tokens,
|
||||
prompt_tokens_details=PromptTokensDetailsWrapper(
|
||||
text_tokens=usage_object.input_token_details.text_tokens,
|
||||
audio_tokens=usage_object.input_token_details.audio_tokens,
|
||||
prompt_tokens_details=(
|
||||
PromptTokensDetailsWrapper(
|
||||
text_tokens=usage_object.input_token_details.text_tokens,
|
||||
audio_tokens=usage_object.input_token_details.audio_tokens,
|
||||
)
|
||||
if usage_object.input_token_details is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -675,6 +675,13 @@ async def create_realtime_transcription_session(
|
|||
)
|
||||
|
||||
transcription_session: Final = {k: v for k, v in body.items() if k != "model"}
|
||||
await _authorize_and_bind_nested_transcription_models(
|
||||
session_data=transcription_session,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
llm_model_list=llm_model_list,
|
||||
llm_router=llm_router,
|
||||
)
|
||||
_set_transcription_model_on_session(session=transcription_session, model=model, create_if_missing=True)
|
||||
data = {"model": model, "transcription_session": transcription_session}
|
||||
|
||||
data = await add_litellm_data_to_request(
|
||||
|
|
|
|||
|
|
@ -3468,7 +3468,7 @@ def get_optional_params_transcription(
|
|||
from litellm.constants import OPENAI_TRANSCRIPTION_PARAMS
|
||||
|
||||
# retrieve all parameters passed to the function
|
||||
passed_params: Final = locals()
|
||||
passed_params: Final = locals().copy()
|
||||
|
||||
passed_params.pop("OPENAI_TRANSCRIPTION_PARAMS")
|
||||
passed_params.pop("model")
|
||||
|
|
|
|||
|
|
@ -661,8 +661,14 @@ def test_transcription_sessions_requires_auth(proxy_app):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("body", [
|
||||
{"input_audio_transcription": {"model": "gpt-realtime-whisper"}},
|
||||
{"model": "gpt-4o-realtime-preview", "audio": {"input": {"transcription": {"model": "gpt-realtime-whisper"}}}},
|
||||
{"input_audio_transcription": {"model": "gpt-4o-realtime-preview"}, "audio": {"input": {"transcription": {"model": "gpt-realtime-whisper"}}}},
|
||||
{"model": "gpt-4o-realtime-preview", "input_audio_transcription": {"model": "gpt-realtime-whisper"}},
|
||||
])
|
||||
async def test_transcription_sessions_rejects_disallowed_resolved_model(
|
||||
proxy_app,
|
||||
proxy_app, body,
|
||||
):
|
||||
proxy_app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_id="test-user",
|
||||
|
|
@ -679,7 +685,7 @@ async def test_transcription_sessions_rejects_disallowed_resolved_model(
|
|||
response = client.post(
|
||||
"/v1/realtime/transcription_sessions",
|
||||
headers={"Authorization": "Bearer sk-test-master-key"},
|
||||
json={"input_audio_transcription": {"model": "gpt-realtime-whisper"}},
|
||||
json=body,
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
|
@ -1032,6 +1038,7 @@ async def test_transcription_sessions_encrypts_client_secret(
|
|||
|
||||
async def _capturing_route(*args, **kwargs):
|
||||
captured_route_type["route_type"] = kwargs.get("route_type")
|
||||
captured_route_type["session"] = kwargs["data"]["transcription_session"]
|
||||
return await mock_route_request_transcription_sessions(*args, **kwargs)
|
||||
|
||||
try:
|
||||
|
|
@ -1054,6 +1061,8 @@ async def test_transcription_sessions_encrypts_client_secret(
|
|||
"/v1/realtime/transcription_sessions",
|
||||
headers={"Authorization": "Bearer sk-test-master-key"},
|
||||
json={
|
||||
"model": "gpt-realtime-whisper",
|
||||
"audio": {"input": {"transcription": {"model": "gpt-live-transcribe", "language": "en"}}},
|
||||
"input_audio_format": "pcm16",
|
||||
"input_audio_transcription": {"model": "gpt-realtime-whisper"},
|
||||
},
|
||||
|
|
@ -1072,6 +1081,10 @@ async def test_transcription_sessions_encrypts_client_secret(
|
|||
assert "upstream_ephemeral_key" in decrypted
|
||||
# Routed through the dedicated transcription_sessions route type.
|
||||
assert captured_route_type["route_type"] == "acreate_realtime_transcription_session"
|
||||
assert captured_route_type["session"]["input_audio_transcription"]["model"] == "gpt-realtime-whisper"
|
||||
assert captured_route_type["session"]["audio"]["input"]["transcription"] == {
|
||||
"model": "gpt-realtime-whisper", "language": "en",
|
||||
}
|
||||
finally:
|
||||
proxy_app.dependency_overrides.pop(user_api_key_auth, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -143,10 +143,12 @@ async def test_atranscription_stream_preserves_duration_for_callback_cost():
|
|||
|
||||
http_client = httpx.AsyncClient(transport=httpx.MockTransport(send_response))
|
||||
openai_client = AsyncOpenAI(api_key="sk-test", base_url="https://example.com/v1", http_client=http_client)
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {}
|
||||
logging_obj.async_success_handler = AsyncMock()
|
||||
logging_obj.async_failure_handler = AsyncMock()
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging
|
||||
|
||||
logging_obj = Logging(
|
||||
model="gpt-transcribe", messages=[], stream=True, call_type="atranscription",
|
||||
start_time=datetime.now(), litellm_call_id="transcription-cost-test", function_id="transcription-cost-test",
|
||||
)
|
||||
audio_file = io.BytesIO()
|
||||
with wave.open(audio_file, "wb") as wav_file:
|
||||
wav_file.setnchannels(1)
|
||||
|
|
@ -167,8 +169,9 @@ async def test_atranscription_stream_preserves_duration_for_callback_cost():
|
|||
await openai_client.close()
|
||||
|
||||
assert [event.type for event in received] == ["transcript.text.delta", "transcript.text.done"]
|
||||
logging_obj.async_success_handler.assert_awaited_once()
|
||||
logged_response = logging_obj.async_success_handler.await_args.kwargs["result"]
|
||||
logged_response = logging_obj.model_call_details["async_complete_streaming_response"]
|
||||
assert logging_obj.model_call_details["response_cost"] == pytest.approx(0.000075)
|
||||
assert logging_obj.model_call_details["standard_logging_object"]["response_cost"] == pytest.approx(0.000075)
|
||||
assert logged_response._hidden_params["audio_transcription_duration"] == pytest.approx(1.0)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue