mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
* test(integration): streamed Bedrock Messages usage cost equals the recorded spend (Pylon #6667) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): echoed cost-map model info is not persisted as deployment overrides (Pylon #6844) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): reset sweep runs on one pod per tick while replicas share the lease (Pylon #6521) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): bedrock post-call guardrail scans streamed Anthropic Messages tool use without 500 (Pylon #6503) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): realtime cached audio tokens bill at the audio cache-read rate (Pylon #6704) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): legacy GET /spend/logs returns at most the 10000 most recent rows and flags truncation (Pylon #6752) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): itemize Responses API cache write tokens as cache creation cost (Pylon #6454) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): migration entrypoint deploys pending migrations before proxy startup (Pylon #6649) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): opted-in team keys stop at the owner's personal budget (Pylon #6641) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): guardrail information stays in the spend log when the caller sends metadata on /v1/messages (Pylon #6614) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): key model allowlist is enforced on Bedrock passthrough routes (Pylon #6419) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): JWT mapped key backfills a null user email from token claims (Pylon #6266) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): scheduled budget reset recovers from a transient DB transport failure (Pylon #6582) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): team key lists models granted through a team access group (Pylon #6044) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): JWT subject without team claim lands in the configured default team (Pylon #5895) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): end-user spend lands for a key without user_id when the auth cache is Redis (Pylon #6021) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): stale-low redis counter still blocks team member over budget (Pylon #5824) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): prompt-carrying spend rows are written in byte-bounded statements (Pylon #6083) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): logs UI session_total_spend sums every round of a multi-round session (Pylon #5928) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): config.yaml guardrails are served by the guardrail usage detail and overview (Pylon #5813) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): plain chat request skips the object permission lookup (Pylon #5965) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): register july accounting regression contracts Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): isolate cost map override clear on owned proxy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): make reset lease claim and db relay refusal deterministic Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): poll pg_stat settle, bound unbanned relay refusals, clear reset lease on teardown Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): bound relay refusals so the budget sweep can reconnect Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: kerry <kerry@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import uuid
|
|
from hashlib import sha256
|
|
from typing import Final
|
|
|
|
import pytest
|
|
import websockets
|
|
from pydantic import BaseModel, ConfigDict, JsonValue
|
|
|
|
import litellm
|
|
from tests.integration._support.client import JSON_OBJECT, Gateway, eventually
|
|
from tests.integration._support.database import read_rows
|
|
from tests.integration._support.upstream import delete_scenario, register_scenario
|
|
from tests.integration.cost_calculation.cost_tracking_case import RealtimeResponse
|
|
|
|
|
|
class RealtimeRates(BaseModel):
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
input_cost_per_token: float
|
|
input_cost_per_audio_token: float
|
|
cache_read_input_token_cost: float
|
|
cache_read_input_audio_token_cost: float | None = None
|
|
output_cost_per_token: float
|
|
output_cost_per_audio_token: float
|
|
|
|
|
|
MODEL: Final = "gpt-realtime-2"
|
|
RATES: Final = RealtimeRates.model_validate(litellm.get_model_info(MODEL, custom_llm_provider="openai"))
|
|
TEXT_RATE: Final = RATES.input_cost_per_token
|
|
AUDIO_RATE: Final = RATES.input_cost_per_audio_token
|
|
CACHED_TEXT_RATE: Final = RATES.cache_read_input_token_cost
|
|
CACHED_AUDIO_RATE: Final = (
|
|
RATES.cache_read_input_token_cost
|
|
if RATES.cache_read_input_audio_token_cost is None
|
|
else RATES.cache_read_input_audio_token_cost
|
|
)
|
|
OUTPUT_TEXT_RATE: Final = RATES.output_cost_per_token
|
|
OUTPUT_AUDIO_RATE: Final = RATES.output_cost_per_audio_token
|
|
INPUT_TEXT_TOKENS: Final = 116
|
|
INPUT_AUDIO_TOKENS: Final = 167
|
|
CACHED_TEXT_TOKENS: Final = 64
|
|
CACHED_AUDIO_TOKENS: Final = 128
|
|
INPUT_TOKENS: Final = INPUT_TEXT_TOKENS + INPUT_AUDIO_TOKENS
|
|
OUTPUT_TEXT_TOKENS: Final = 8
|
|
OUTPUT_AUDIO_TOKENS: Final = 12
|
|
OUTPUT_TOKENS: Final = OUTPUT_TEXT_TOKENS + OUTPUT_AUDIO_TOKENS
|
|
EXPECTED_INPUT_COST: Final = (
|
|
(INPUT_TEXT_TOKENS - CACHED_TEXT_TOKENS) * TEXT_RATE
|
|
+ CACHED_TEXT_TOKENS * CACHED_TEXT_RATE
|
|
+ (INPUT_AUDIO_TOKENS - CACHED_AUDIO_TOKENS) * AUDIO_RATE
|
|
+ CACHED_AUDIO_TOKENS * CACHED_AUDIO_RATE
|
|
)
|
|
EXPECTED_OUTPUT_COST: Final = OUTPUT_TEXT_TOKENS * OUTPUT_TEXT_RATE + OUTPUT_AUDIO_TOKENS * OUTPUT_AUDIO_RATE
|
|
|
|
|
|
def cached_audio_response_done() -> RealtimeResponse:
|
|
return RealtimeResponse(
|
|
content_type="application/x-realtime",
|
|
events=(
|
|
{
|
|
"type": "response.done",
|
|
"event_id": "evt_$REQUEST_ID",
|
|
"response": {
|
|
"id": "resp_$REQUEST_ID",
|
|
"object": "realtime.response",
|
|
"status": "completed",
|
|
"output": [],
|
|
"usage": {
|
|
"total_tokens": INPUT_TOKENS + OUTPUT_TOKENS,
|
|
"input_tokens": INPUT_TOKENS,
|
|
"output_tokens": OUTPUT_TOKENS,
|
|
"input_token_details": {
|
|
"text_tokens": INPUT_TEXT_TOKENS,
|
|
"audio_tokens": INPUT_AUDIO_TOKENS,
|
|
"cached_tokens": CACHED_TEXT_TOKENS + CACHED_AUDIO_TOKENS,
|
|
"cached_tokens_details": {
|
|
"text_tokens": CACHED_TEXT_TOKENS,
|
|
"audio_tokens": CACHED_AUDIO_TOKENS,
|
|
},
|
|
},
|
|
"output_token_details": {
|
|
"text_tokens": OUTPUT_TEXT_TOKENS,
|
|
"audio_tokens": OUTPUT_AUDIO_TOKENS,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
async def _one_realtime_turn(proxy_url: str, key: str, model: str) -> dict[str, JsonValue]:
|
|
async with websockets.connect(
|
|
f"{proxy_url.replace('http://', 'ws://').replace('https://', 'wss://')}/v1/realtime?model={model}",
|
|
additional_headers={"Authorization": f"Bearer {key}"},
|
|
) as websocket:
|
|
session: Final = JSON_OBJECT.validate_json(await websocket.recv())
|
|
await websocket.send(json.dumps({"type": "response.create"}))
|
|
async for message in websocket:
|
|
if JSON_OBJECT.validate_json(message).get("type") == "response.done":
|
|
return session
|
|
raise AssertionError(f"websocket closed before response.done for {model}")
|
|
|
|
|
|
@pytest.mark.covers("pricing.realtime.cached_audio_tokens_bill_at_audio_cache_read_rate")
|
|
def test_realtime_cached_audio_tokens_bill_at_audio_cache_read_rate_not_full_audio_rate(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
scenario_id: Final = f"realtime-cached-audio-{uuid.uuid4().hex[:12]}"
|
|
handle: Final = register_scenario(scenario_id, cached_audio_response_done())
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
key: Final = scenario.key()
|
|
model: Final = scenario.model(
|
|
model=f"openai/{MODEL}", api_key=scenario_id, api_base=gateway.upstream_url.rstrip("/")
|
|
)
|
|
session: Final = asyncio.run(_one_realtime_turn(os.environ["INTEGRATION_PROXY_URL"].rstrip("/"), key, model))
|
|
assert session.get("type") == "session.created", session
|
|
rows: Final = eventually(
|
|
lambda: read_rows(
|
|
'SELECT spend, prompt_tokens, completion_tokens, call_type FROM "LiteLLM_SpendLogs" WHERE api_key = %s',
|
|
(sha256(key.encode()).hexdigest(),),
|
|
),
|
|
lambda values: len(values) == 1,
|
|
seconds=70,
|
|
)
|
|
assert rows[0]["call_type"] == "_arealtime", rows
|
|
assert rows[0]["prompt_tokens"] == INPUT_TOKENS, rows
|
|
assert rows[0]["completion_tokens"] == OUTPUT_TOKENS, rows
|
|
assert float(str(rows[0]["spend"])) == pytest.approx(EXPECTED_INPUT_COST + EXPECTED_OUTPUT_COST, rel=1e-6), rows
|