mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-17 04:17:43 +00:00
* test: modernize models used in CircleCI e2e test suites
Replaces obsolete models (gpt-4o, gpt-4o-mini, gpt-3.5-turbo,
claude-3-5-sonnet-20240620, claude-sonnet-4-20250514) with current
equivalents across the e2e_openai_endpoints and
proxy_e2e_anthropic_messages_tests CircleCI jobs.
- gpt-4o -> gpt-5.5 (responses API e2e tests)
- gpt-4o-mini -> gpt-5-mini (websocket responses, oai_misc_config)
- gpt-4o-mini-2024-07-18 -> gpt-4.1-mini-2025-04-14 (fine-tuning,
still actively fine-tunable)
- gpt-4 / gpt-3.5-turbo target_model_names example -> gpt-5.5 /
gpt-5-mini
- bedrock claude-3-5-sonnet-20240620 batch entry -> haiku-4-5-20251001
(also aligning oai_misc_config model_name with what
test_bedrock_batches_api.py actually requests)
- bedrock claude-sonnet-4-20250514 (deprecated, retires 2026-06-15)
-> claude-sonnet-4-5-20250929
* test: point bedrock-claude-sonnet-4 alias at Sonnet 4.6, not 4.5
Greptile/Cursor flagged that after the previous commit, the
bedrock-claude-sonnet-4 alias collided with bedrock-claude-sonnet-4.5
(both pointed to claude-sonnet-4-5-20250929). Rename to
bedrock-claude-sonnet-4.6 and point it at the Sonnet 4.6 Bedrock ID
(us.anthropic.claude-sonnet-4-6, already in the litellm model
registry) so the alias name matches the underlying model version.
* test: modernize models across remaining CI-mounted configs & tests
Expands the modernization sweep to all CircleCI-mounted proxy configs
and to test directories where the model literal is a fixture/route key
(not the test's subject).
Config changes:
- proxy_server_config.yaml: bump gpt-3.5-turbo / gpt-3.5-turbo-1106 /
gpt-4o / gemini-1.5-flash / dall-e-3 underlying models; rename
gpt-3.5-turbo-end-user-test alias to gpt-5-mini-end-user-test; bump
text-embedding-ada-002 underlying to text-embedding-3-small. User-
facing aliases (gpt-3.5-turbo, gpt-4, text-embedding-ada-002, etc.)
preserved for backward compatibility with tests.
- simple_config.yaml, otel_test_config.yaml, spend_tracking_config.yaml:
bump gpt-3.5-turbo underlying to gpt-5-mini.
- pass_through_config.yaml: claude-3-5-sonnet / claude-3-7-sonnet /
claude-3-haiku entries replaced with claude-sonnet-4-5 / claude-
haiku-4-5 / claude-opus-4-7.
- oai_misc_config.yaml: align alias name with the gpt-5-mini rename.
Test changes (proactive: claude-sonnet-4-20250514 / claude-opus-4-
20250514 retire 2026-06-15):
- tests/llm_translation/test_anthropic_completion.py: bump 3 references
+ paired Vertex AI ID to claude-sonnet-4-5.
- tests/llm_translation/test_optional_params.py: bump 2 references.
- tests/pass_through_unit_tests/test_anthropic_messages_passthrough.py
and test_bedrock_anthropic_messages_test.py: bump router fixtures
using the deprecated model IDs.
- tests/pass_through_unit_tests/base_anthropic_messages_tool_search_test.py:
modernize docstring examples.
- tests/test_end_users.py: update references to renamed alias.
* test: modernize placeholder model literals in router_unit_tests
Mass replace_all on fixture/placeholder model literals across the
router_unit_tests/ suite (model name is a routing key / label, not the
test subject). Sub-agent sweep so far — additional commits will follow
for logging_callback_tests/, enterprise/, top-level tests/test_*.py,
and other CI-mounted dirs.
Mappings applied:
- gpt-3.5-turbo -> gpt-5-mini
- gpt-4 (bare) -> gpt-5.5
- gpt-4o (bare) -> gpt-5
- text-embedding-ada-002 -> text-embedding-3-small
- claude-3-sonnet-20240229 / claude-3-opus-20240229 /
claude-3-haiku-20240307 / claude-3-5-sonnet-20240620 ->
claude-sonnet-4-5-20250929 / claude-opus-4-7 /
claude-haiku-4-5-20251001 as appropriate
Explicitly preserved:
- gpt-4o-mini-* variants (transcribe, tts, etc.) where they're current
- gpt-4-turbo / gpt-4-vision-preview / gpt-4-0613 (subject literals)
- JSONL batch body literals
- Mock LLM response model fields (must match upstream)
- Fake/mock identifiers
* test: modernize placeholder model literals across remaining CI suites
Sub-agent sweep across logging_callback_tests/, guardrails_tests/,
enterprise/, pass_through_unit_tests/, otel_tests/,
llm_responses_api_testing/, batches_tests/, spend_tracking_tests/,
litellm_utils_tests/, unified_google_tests/, and a few top-level
tests/test_*.py files where the model literal is a fixture or
placeholder (router model_list, mock standard logging payload, mock
callback data) rather than the test's subject.
Mappings applied (see scope notes below):
- gpt-3.5-turbo -> gpt-5-mini
- gpt-4 (bare) -> gpt-5.5
- gpt-4o (bare) -> gpt-5.5 (corrected from initial gpt-5 — bare gpt-5
is not a valid OpenAI alias; only gpt-5.5 / gpt-5.4 / gpt-5.2-codex
/ gpt-5-mini exist)
- gpt-4o-mini (bare) -> gpt-5-mini
- text-embedding-ada-002 -> text-embedding-3-small
- claude-3-sonnet-20240229 -> claude-sonnet-4-5-20250929
- claude-3-opus-20240229 -> claude-opus-4-7
- claude-3-haiku-20240307 -> claude-haiku-4-5-20251001
- claude-3-5-sonnet-20240620/20241022 -> claude-sonnet-4-5-20250929
- claude-3-7-sonnet-20250219 -> claude-sonnet-4-6
- gemini-1.5-flash -> gemini-2.5-flash
- gemini-1.5-pro -> gemini-2.5-pro
Explicitly preserved (not modernized):
- llm_translation/ tests where model is the SUBJECT (provider-specific
translation/transformation logic). Only the deprecated 20250514
references were already bumped in a prior commit.
- Cost-calc / tokenizer subject tests in test_utils.py (skip-ranges
documented by the sub-agent).
- Bedrock model IDs in test_health_check.py path-stripping tests.
- JSONL batch request bodies and mock LLM response bodies (must match
upstream literal).
- Langfuse expected-request-body JSON fixtures (cost values are exact-
match-asserted; changing the model would shift response_cost).
- gpt-3.5-turbo-instruct (text-completion endpoint; no modern OpenAI
equivalent).
- Top-level tests calling the proxy through user-facing aliases
(gpt-3.5-turbo, gpt-4, text-embedding-ada-002, dall-e-3) — aliases
in proxy_server_config.yaml stay; only the underlying model was
bumped.
- tests/test_gpt5_azure_temperature_support.py (the test's whole point
is model-name handling).
- Fake / mock / openai/fake identifiers.
Notable side fixes:
- test_spend_accuracy_tests.py: UPSTREAM_MODEL now matches what
spend_tracking_config.yaml's proxy actually routes to (gpt-5-mini),
resolving a latent inconsistency.
- proxy_server_config.yaml: bare `gpt-5` alias renamed to `gpt-5.5`
(bare gpt-5 is not a valid OpenAI alias).
- test_batches_logging_unit_tests.py: explicit_models list entries
kept distinct (gpt-5-mini + gpt-5.5) after bulk rename.
* test: fix CI failures from model modernization sweep
CI surfaced 4 categories of regression from the bulk modernization:
1. Azure deployment names are customer-specific. Reverted:
- tests/litellm_utils_tests/test_health_check.py: azure/text-
embedding-3-small -> azure/text-embedding-ada-002 (the CI Azure
account does not have a text-embedding-3-small deployment).
- tests/logging_callback_tests/test_custom_callback_router.py:
same revert for two router fixtures driving aembedding.
2. gpt-5 family does not accept temperature != 1. Tests that pass a
custom temperature swapped from gpt-5-mini to gpt-4.1-mini (modern
non-reasoning OpenAI mini that still accepts temperature/logprobs):
- tests/logging_callback_tests/test_datadog.py
- tests/logging_callback_tests/test_langsmith_unit_test.py
- tests/logging_callback_tests/test_otel_logging.py
3. proxy_server_config.yaml's gpt-3.5-turbo-large alias was routing to
gpt-5.5 (a reasoning model that rejects logprobs). The proxy test
tests/test_openai_endpoints.py::test_chat_completion_streaming
exercises logprobs/top_logprobs through that alias. Bumped the
underlying model to gpt-4.1 (non-reasoning, still modern).
4. tests/logging_callback_tests/test_gcs_pub_sub.py asserts against a
pinned JSON fixture (gcs_pub_sub_body/spend_logs_payload.json) with
hardcoded model="gpt-4o" and a model-specific spend value. Reverted
the litellm.acompletion calls in the test to model="gpt-4o" so the
fixture's exact-match assertions still hold.
5. tests/pass_through_unit_tests/test_anthropic_messages_passthrough.py:
anthropic.messages.create routing to openai/gpt-5-mini returned an
empty content[0] with max_tokens=100 (reasoning-token consumption).
Swapped to openai/gpt-4.1-mini.
* test: fix Assistants API model + 2 cursor[bot] review nits
1. pass_through_unit_tests/test_custom_logger_passthrough.py: gpt-5.5
isn't accepted by the /v1/assistants endpoint
("unsupported_model"). Switch to gpt-4.1-mini (modern, Assistants-
API-supported, non-reasoning).
2. example_config_yaml/pass_through_config.yaml: the previous sweep
bumped the claude-3-7-sonnet alias to claude-opus-4-7, which is a
tier change (Sonnet -> Opus). Map to claude-sonnet-4-6 to keep the
Sonnet tier intact. (Cursor bugbot review.)
3. example_config_yaml/simple_config.yaml: model_name was left as
gpt-3.5-turbo while the underlying was bumped to gpt-5-mini, which
muddles the "simple" example. Make both sides gpt-5-mini so the
most basic example is a straight 1:1 mapping again. (Cursor bugbot
review.)
* fix: revert gpt-4/gpt-3.5-turbo alias underlying to non-reasoning models
tests/test_openai_endpoints.py::test_completion calls the proxy alias
"gpt-4" with temperature=0, and other tests call gpt-3.5-turbo with
custom temperature / logprobs / the legacy /v1/completions endpoint.
The earlier modernization mapped both aliases to gpt-5.5 / gpt-5-mini,
which are reasoning models that reject temperature != 1 and don't
expose /v1/completions. Map the aliases to gpt-4.1 / gpt-4.1-mini
(modern non-reasoning OpenAI models) instead — keeps user-facing
aliases preserved while picking a current underlying that still
supports the parameters/endpoints the tests exercise.
396 lines
16 KiB
Python
396 lines
16 KiB
Python
import pytest
|
|
import asyncio
|
|
import aiohttp
|
|
import time
|
|
|
|
import litellm
|
|
from litellm._uuid import uuid
|
|
|
|
"""
|
|
Tests to run
|
|
|
|
Basic Tests:
|
|
1. Basic Spend Accuracy Test:
|
|
- Make N requests, compute expected total spend locally from each response's usage
|
|
- Poll until batch writer has flushed spend to the DB
|
|
- Expect spend for Key, Team, User, Org (/info endpoints) to equal the computed total
|
|
|
|
2. Long term spend accuracy test (with 2 bursts of requests)
|
|
- Burst 1: compute expected from responses, verify
|
|
- Burst 2: compute expected from responses, verify total = burst1 + burst2
|
|
|
|
Additional Test Scenarios:
|
|
|
|
3. Concurrent Request Accuracy Test:
|
|
- Make 20 concurrent requests
|
|
- Check for race conditions in spend tracking
|
|
|
|
4. Error Case Test:
|
|
- Make 10 successful requests
|
|
- Make 5 failed requests
|
|
- Verify spend is only counted for successful requests
|
|
|
|
5. Mixed Request Type Test:
|
|
- Make different types of requests with varying costs
|
|
- Verify accurate total spend calculation
|
|
"""
|
|
|
|
# Upstream model the proxy is configured with (spend_tracking_config.yaml).
|
|
# The proxy computes spend using this model's pricing; the local ground-truth
|
|
# calculation uses the same pricing table via litellm.cost_per_token.
|
|
UPSTREAM_MODEL = "gpt-5-mini"
|
|
|
|
# Batch writer flush cadence in CI is ~2-7s (PROXY_BATCH_WRITE_AT=2 + up to 5s jitter).
|
|
# Poll every 2s for 60s — plenty of headroom for multiple ticks to land.
|
|
POLL_INTERVAL_SECONDS = 2
|
|
POLL_TIMEOUT_SECONDS = 60
|
|
|
|
TOLERANCE = 1e-10
|
|
|
|
|
|
def _make_test_session() -> aiohttp.ClientSession:
|
|
"""
|
|
Session tuned for CI reliability:
|
|
- force_close: avoid aiohttp reusing a TCP connection that the proxy/kernel
|
|
silently closed during the long idle window between setup POSTs and the
|
|
later poll loop (observed failure mode: ConnectionTimeoutError on the
|
|
first /key/info call after 20 chat completions).
|
|
- explicit connect timeout: surface a blocked proxy event loop quickly
|
|
instead of hanging on aiohttp's 5-minute default total timeout.
|
|
"""
|
|
return aiohttp.ClientSession(
|
|
connector=aiohttp.TCPConnector(force_close=True),
|
|
timeout=aiohttp.ClientTimeout(total=30, connect=10),
|
|
)
|
|
|
|
|
|
async def create_organization(session, organization_alias: str):
|
|
"""Helper function to create a new organization"""
|
|
url = "http://0.0.0.0:4000/organization/new"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
data = {"organization_alias": organization_alias}
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
return await response.json()
|
|
|
|
|
|
async def create_team(session, org_id: str):
|
|
"""Helper function to create a new team under an organization"""
|
|
url = "http://0.0.0.0:4000/team/new"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
data = {"organization_id": org_id, "team_alias": f"test-team-{uuid.uuid4()}"}
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
return await response.json()
|
|
|
|
|
|
async def create_user(session, org_id: str):
|
|
"""Helper function to create a new user"""
|
|
url = "http://0.0.0.0:4000/user/new"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
data = {"user_name": f"test-user-{uuid.uuid4()}"}
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
return await response.json()
|
|
|
|
|
|
async def generate_key(session, user_id: str, team_id: str):
|
|
"""Helper function to generate a key for a specific user and team"""
|
|
url = "http://0.0.0.0:4000/key/generate"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
data = {"user_id": user_id, "team_id": team_id}
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
return await response.json()
|
|
|
|
|
|
async def chat_completion(session, key: str):
|
|
"""Make a chat completion request"""
|
|
from openai import AsyncOpenAI
|
|
from litellm._uuid import uuid
|
|
|
|
client = AsyncOpenAI(api_key=key, base_url="http://0.0.0.0:4000/v1")
|
|
|
|
response = await client.chat.completions.create(
|
|
model="fake-openai-endpoint",
|
|
messages=[{"role": "user", "content": f"Test message {uuid.uuid4()}"}],
|
|
)
|
|
return response
|
|
|
|
|
|
async def get_spend_info(session, entity_type: str, entity_id: str):
|
|
"""Helper function to get spend information for an entity"""
|
|
url = f"http://0.0.0.0:4000/{entity_type}/info"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
if entity_type == "key":
|
|
data = {"key": entity_id}
|
|
else:
|
|
data = {f"{entity_type}_id": entity_id}
|
|
|
|
async with session.get(url, headers=headers, params=data) as response:
|
|
return await response.json()
|
|
|
|
|
|
async def get_proxy_readiness(session):
|
|
"""Fetch authenticated readiness details. Used both as a fail-fast gate and as a diagnostic on poll timeout."""
|
|
url = "http://0.0.0.0:4000/health/readiness/details"
|
|
headers = {"Authorization": "Bearer sk-1234"}
|
|
async with session.get(url, headers=headers) as response:
|
|
return response.status, await response.json()
|
|
|
|
|
|
async def assert_proxy_healthy(session):
|
|
"""Fail fast if the proxy's DB or cache is not reachable — no point running the test."""
|
|
status, body = await get_proxy_readiness(session)
|
|
if status != 200 or body.get("db") != "connected":
|
|
pytest.fail(
|
|
f"Proxy /health/readiness/details unhealthy (status={status}). "
|
|
f"Cannot run spend accuracy test. Response: {body}"
|
|
)
|
|
print(f"Proxy readiness OK: {body}")
|
|
|
|
|
|
def compute_expected_spend(responses) -> float:
|
|
"""
|
|
Compute the expected total spend locally from each response's usage tokens,
|
|
using the same pricing table the proxy uses. This is the independent ground
|
|
truth we compare the proxy's reported spend against.
|
|
"""
|
|
total = 0.0
|
|
for r in responses:
|
|
usage = r.usage
|
|
prompt_cost, completion_cost = litellm.cost_per_token(
|
|
model=UPSTREAM_MODEL,
|
|
prompt_tokens=usage.prompt_tokens,
|
|
completion_tokens=usage.completion_tokens,
|
|
)
|
|
total += prompt_cost + completion_cost
|
|
return total
|
|
|
|
|
|
async def poll_key_spend_until(session, key: str, expected: float) -> float:
|
|
"""
|
|
Poll key spend until it matches `expected` within TOLERANCE, or timeout.
|
|
Returns the last observed spend either way; caller decides how to report.
|
|
"""
|
|
start = time.time()
|
|
last_spend = 0.0
|
|
while time.time() - start < POLL_TIMEOUT_SECONDS:
|
|
try:
|
|
key_info = await get_spend_info(session, "key", key)
|
|
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
|
print(
|
|
f"Transient transport error during spend poll: "
|
|
f"{type(exc).__name__}: {exc}. Retrying... "
|
|
f"({time.time() - start:.1f}s elapsed)"
|
|
)
|
|
await asyncio.sleep(POLL_INTERVAL_SECONDS)
|
|
continue
|
|
last_spend = key_info["info"]["spend"]
|
|
if abs(last_spend - expected) < TOLERANCE:
|
|
print(
|
|
f"Key spend reached expected {expected} after {time.time() - start:.1f}s"
|
|
)
|
|
return last_spend
|
|
print(
|
|
f"Key spend {last_spend}, expected {expected}, waiting... "
|
|
f"({time.time() - start:.1f}s elapsed)"
|
|
)
|
|
await asyncio.sleep(POLL_INTERVAL_SECONDS)
|
|
return last_spend
|
|
|
|
|
|
async def fail_with_diagnostics(session, stage: str, expected: float, observed: float):
|
|
"""Emit a failure with readiness state so CI output points at the real cause."""
|
|
_, readiness = await get_proxy_readiness(session)
|
|
pytest.fail(
|
|
f"{stage}: key spend did not match expected after {POLL_TIMEOUT_SECONDS}s poll. "
|
|
f"expected={expected}, observed={observed}, diff={expected - observed}. "
|
|
f"Proxy readiness: {readiness}"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_basic_spend_accuracy():
|
|
"""
|
|
Test basic spend accuracy across different entities:
|
|
1. Create org, team, user, and key
|
|
2. Make N requests, keeping each response
|
|
3. Compute expected spend locally from response usage (independent ground truth)
|
|
4. Poll until proxy-reported spend matches expected
|
|
5. Verify spend is consistent across key, team, user, and org entities
|
|
"""
|
|
NUM_LLM_REQUESTS = 20
|
|
|
|
async with _make_test_session() as session:
|
|
await assert_proxy_healthy(session)
|
|
|
|
org_response = await create_organization(
|
|
session=session, organization_alias=f"test-org-{uuid.uuid4()}"
|
|
)
|
|
print("org_response: ", org_response)
|
|
org_id = org_response["organization_id"]
|
|
|
|
team_response = await create_team(session, org_id)
|
|
print("team_response: ", team_response)
|
|
team_id = team_response["team_id"]
|
|
|
|
user_response = await create_user(session, org_id)
|
|
print("user_response: ", user_response)
|
|
user_id = user_response["user_id"]
|
|
|
|
key_response = await generate_key(session, user_id, team_id)
|
|
print("key_response: ", key_response)
|
|
key = key_response["key"]
|
|
|
|
responses = []
|
|
for i in range(NUM_LLM_REQUESTS):
|
|
response = await chat_completion(session, key)
|
|
responses.append(response)
|
|
print(f"Request {i + 1}/{NUM_LLM_REQUESTS} completed")
|
|
|
|
expected_spend = compute_expected_spend(responses)
|
|
assert expected_spend > 0, (
|
|
f"Locally computed expected spend is {expected_spend}. Either cost calc "
|
|
f"is broken or upstream returned zero tokens. "
|
|
f"Usage: {[r.usage.model_dump() for r in responses]}"
|
|
)
|
|
print(f"Expected total spend (local ground truth): {expected_spend}")
|
|
|
|
final_spend = await poll_key_spend_until(session, key, expected_spend)
|
|
if abs(final_spend - expected_spend) >= TOLERANCE:
|
|
await fail_with_diagnostics(
|
|
session,
|
|
stage="test_basic_spend_accuracy",
|
|
expected=expected_spend,
|
|
observed=final_spend,
|
|
)
|
|
|
|
# Allow a final scheduler tick for team/user/org aggregations to settle
|
|
await asyncio.sleep(5)
|
|
|
|
key_info = await get_spend_info(session, "key", key)
|
|
print("key_info: ", key_info)
|
|
team_info = await get_spend_info(session, "team", team_id)
|
|
print("team_info: ", team_info)
|
|
user_info = await get_spend_info(session, "user", user_id)
|
|
print("user_info: ", user_info)
|
|
org_info = await get_spend_info(session, "organization", org_id)
|
|
print("org_info: ", org_info)
|
|
|
|
assert (
|
|
abs(key_info["info"]["spend"] - expected_spend) < TOLERANCE
|
|
), f"Key spend {key_info['info']['spend']} does not match expected {expected_spend}"
|
|
|
|
assert (
|
|
abs(user_info["user_info"]["spend"] - expected_spend) < TOLERANCE
|
|
), f"User spend {user_info['user_info']['spend']} does not match expected {expected_spend}"
|
|
|
|
assert (
|
|
abs(team_info["team_info"]["spend"] - expected_spend) < TOLERANCE
|
|
), f"Team spend {team_info['team_info']['spend']} does not match expected {expected_spend}"
|
|
|
|
assert (
|
|
abs(org_info["spend"] - expected_spend) < TOLERANCE
|
|
), f"Organization spend {org_info['spend']} does not match expected {expected_spend}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_long_term_spend_accuracy_with_bursts():
|
|
"""
|
|
Test long-term spend accuracy with multiple bursts of requests:
|
|
1. Create org, team, user, and key
|
|
2. Burst 1: make requests, compute expected locally, verify proxy matches
|
|
3. Burst 2: make more requests, verify proxy total == burst1 + burst2
|
|
4. Verify total spend is consistent across all entities
|
|
"""
|
|
BURST_1_REQUESTS = 22
|
|
BURST_2_REQUESTS = 12
|
|
|
|
async with _make_test_session() as session:
|
|
await assert_proxy_healthy(session)
|
|
|
|
org_response = await create_organization(
|
|
session=session, organization_alias=f"test-org-{uuid.uuid4()}"
|
|
)
|
|
print("org_response: ", org_response)
|
|
org_id = org_response["organization_id"]
|
|
|
|
team_response = await create_team(session, org_id)
|
|
print("team_response: ", team_response)
|
|
team_id = team_response["team_id"]
|
|
|
|
user_response = await create_user(session, org_id)
|
|
print("user_response: ", user_response)
|
|
user_id = user_response["user_id"]
|
|
|
|
key_response = await generate_key(session, user_id, team_id)
|
|
print("key_response: ", key_response)
|
|
key = key_response["key"]
|
|
|
|
print(f"Starting first burst of {BURST_1_REQUESTS} requests...")
|
|
burst_1_responses = []
|
|
for i in range(BURST_1_REQUESTS):
|
|
response = await chat_completion(session, key)
|
|
burst_1_responses.append(response)
|
|
print(f"Burst 1 - Request {i + 1}/{BURST_1_REQUESTS} completed")
|
|
|
|
burst_1_expected = compute_expected_spend(burst_1_responses)
|
|
assert burst_1_expected > 0, (
|
|
f"Burst 1 expected spend is {burst_1_expected}. "
|
|
f"Usage: {[r.usage.model_dump() for r in burst_1_responses]}"
|
|
)
|
|
print(f"Burst 1 expected spend: {burst_1_expected}")
|
|
|
|
final_burst_1 = await poll_key_spend_until(session, key, burst_1_expected)
|
|
if abs(final_burst_1 - burst_1_expected) >= TOLERANCE:
|
|
await fail_with_diagnostics(
|
|
session,
|
|
stage="test_long_term_spend_accuracy burst 1",
|
|
expected=burst_1_expected,
|
|
observed=final_burst_1,
|
|
)
|
|
|
|
print(f"Starting second burst of {BURST_2_REQUESTS} requests...")
|
|
burst_2_responses = []
|
|
for i in range(BURST_2_REQUESTS):
|
|
response = await chat_completion(session, key)
|
|
burst_2_responses.append(response)
|
|
print(f"Burst 2 - Request {i + 1}/{BURST_2_REQUESTS} completed")
|
|
|
|
total_expected = burst_1_expected + compute_expected_spend(burst_2_responses)
|
|
print(f"Total expected spend (burst 1 + burst 2): {total_expected}")
|
|
|
|
final_total = await poll_key_spend_until(session, key, total_expected)
|
|
if abs(final_total - total_expected) >= TOLERANCE:
|
|
await fail_with_diagnostics(
|
|
session,
|
|
stage="test_long_term_spend_accuracy total",
|
|
expected=total_expected,
|
|
observed=final_total,
|
|
)
|
|
|
|
await asyncio.sleep(5)
|
|
|
|
key_info = await get_spend_info(session, "key", key)
|
|
team_info = await get_spend_info(session, "team", team_id)
|
|
user_info = await get_spend_info(session, "user", user_id)
|
|
org_info = await get_spend_info(session, "organization", org_id)
|
|
|
|
print(f"Final key spend: {key_info['info']['spend']}")
|
|
print(f"Final team spend: {team_info['team_info']['spend']}")
|
|
print(f"Final user spend: {user_info['user_info']['spend']}")
|
|
print(f"Final org spend: {org_info['spend']}")
|
|
|
|
assert (
|
|
abs(key_info["info"]["spend"] - total_expected) < TOLERANCE
|
|
), f"Key spend {key_info['info']['spend']} does not match expected {total_expected}"
|
|
|
|
assert (
|
|
abs(user_info["user_info"]["spend"] - total_expected) < TOLERANCE
|
|
), f"User spend {user_info['user_info']['spend']} does not match expected {total_expected}"
|
|
|
|
assert (
|
|
abs(team_info["team_info"]["spend"] - total_expected) < TOLERANCE
|
|
), f"Team spend {team_info['team_info']['spend']} does not match expected {total_expected}"
|
|
|
|
assert (
|
|
abs(org_info["spend"] - total_expected) < TOLERANCE
|
|
), f"Organization spend {org_info['spend']} does not match expected {total_expected}"
|