litellm/tests/e2e
Sameer Kankute 4476923ac4
test: add realtime proxy e2e suite across providers (#30960)
* tests: add e2e tests for spend, budgets and llms

* style: make chained comparison of status_code clearer

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* remove e2e_tests folder

* test: add spend tracking tests

* test: multi-window budgets coverage

* fix: p0 issues, added types and shared functions for each test suite

* chore: add config.yml

* test: passthrough endpoints stream/non-stream e2e

* style: carry clearer status_code comparison into renamed e2e dir

* fix: rename cost breakdown function

* fix: pydantic validation for budget info, dont allow explicit type cast

* refactor: migrate to gateway client

* test: add custom pricing tests

* chore: change master key

* test(e2e): address greptile review feedback

Remove the duplicate cache/cache_params block in the gateway config so the two
can't silently diverge under future edits. Reorder the soft-budget test to assert
the call isn't a budget block before require_successful_call, since that helper
hard-fails any non-2xx and left the budget-block check unreachable; the misleading
"skip" comment is corrected. Add a deferred delete in test_budget_delete_removes_it
so a failed delete doesn't leak a budget on the shared proxy. Scope the
spend_tracking sys.path insertion in pytest_sessionfinish to just the cleanup
import so a broader "pytest tests/" run isn't left with a mutated path.

* test(e2e): drop misleading skip comment on require_successful_call

require_successful_call fails hard, it does not skip; the trailing
comment was factually wrong. The function name already states intent,
so the comment is removed in both per-model and tag budget helpers.

* test(e2e): assert budget-isolation invariant before success check

On the should-still-succeed path of the per-model and tag isolation
tests, check is_budget_block before require_successful_call. If the
isolation bug fires the unaffected model/tag is blocked, so asserting
the specific 'blocked by X' invariant first yields the diagnostic
message instead of a generic upstream-failure. Matches the ordering in
test_soft_budget_e2e.py.

* fix(e2e): guard spend-log truncate on skip and stop returning unrelated priced rows

* fix(e2e): run case init() inside try so partial-init failures tear down

run_case called case.init() outside the try/finally that runs teardown(), so a
case that registers cleanups progressively (create team, then user, then key)
and then fails partway through init() would leak the already-created entities on
the long-lived shared proxy. Move init() inside the try so teardown always runs.

Add a regression test that registers a cleanup then raises mid-init and asserts
the resource is still released.

* test(e2e): mark known pricing-leak isolation test xfail(strict)

test_custom_pricing_is_isolated_from_sibling_deployment documents a real proxy
gap (a deployment's custom per-token pricing leaks into the shared cost map for
sibling deployments of the same underlying model) and was left unconditionally
failing, which pollutes the suite's pass/fail signal. Mark it xfail(strict=True)
so the suite stays green while the leak persists and turns into a failure the
moment isolation is fixed, prompting the marker's removal.

* refactor(e2e): make suite pass its shipped strict basedpyright config

The suite ships tests/pyrightconfig.json (strict, no Any), but basedpyright
--project tests reported four errors in it: three reportAny on the parametrize
ids=lambda c: c.__name__, and one reportUnusedFunction on the underscore-prefixed
autouse fixture _require_live_proxy. Replace the untyped lambda with a typed
_case_id(case_cls: Type[_BudgetCase]) -> str so the ids are no longer Any, and
rename the fixture to require_live_proxy so basedpyright no longer treats it as an
unused private function (it is referenced only by pytest's autouse machinery).
basedpyright --project tests now reports zero errors.

* fix(tests/e2e): gate spend-log truncate on e2e marker, not test directory

* test(e2e): run harness unit tests without a live proxy

The autouse session fixture skipped the whole tests/e2e session when no proxy
answered, which also skipped test_lifecycle.py, a pure unit test of run_case that
never touches the proxy. A regression test that silently skips gives no signal,
so the skip now lives in pytest_runtest_setup gated on the same e2e marker the
spend-log truncate guard already uses: live tests skip when no proxy is up while
harness unit coverage always runs. The liveness probe is cached with lru_cache so
it still runs once per session

* test(e2e): clean up gateway config comment debris

Fix the typo on the header comment and drop the orphaned namespace/ttl
comment remnants left indented under cache_params; the active values are
already set above. Flagged by greptile review.

* fix: add new tests, split gateway

* test(e2e): type the redis spend-counter probe for strict basedpyright

The new cold-counter reseed test drove its redis client untyped, so the strict
tests/pyrightconfig.json (reportUnknown*, reportAny) flagged ten errors once the
file landed: scan_iter/get came back unknown and the pool.map lambda had an
untyped parameter. Annotate the client as redis.Redis[str] via a TYPE_CHECKING
import (the runtime import stays lazy so the suite still skips, not errors, when
redis is absent), which resolves scan_iter to Iterator[str] and get to str | None,
and replace the lambda with a typed inner function mirroring _burst. basedpyright
--project tests is back to zero errors.

* test(e2e): xfail the known team multi-window failure and isolate member teardown

Greptile flagged two issues in the mirrored split-gateway commit. The team
multi-window budget test documents a real /team/new write bug (budget_limits go
straight to the Json? column and Prisma 500s, unlike the json.dumps'd key and
/team/update paths) and was left as an unconditional hard failure, which would
turn any live-proxy CI run red; mark it xfail(strict=True) like the custom-pricing
isolation test so the suite stays green while the bug persists and flips to a
failure the moment the write is fixed and the marker should go.

The class-scoped member fixture in test_team_member_budget_e2e.py tore down its
key, user, and team sequentially with no exception isolation, so a failed
delete_key would strand the user and team on the long-lived shared proxy. Route
cleanup through a ResourceManager: register each delete progressively and run them
LIFO best-effort in a finally, so a partial-setup failure still releases what came
before and one failed delete never blocks the rest.

* test: add realtime proxy e2e suite across providers

Add tests/realtime_e2e covering the proxy realtime websocket endpoint
end to end against live providers (openai, azure, gemini, vertex_ai,
bedrock, xai). Two layers: a raw-websocket suite asserting the
normalized OpenAI GA event sequence, delta/transcript consistency,
usage, and a full tool-call round-trip; and a pipecat smoke driving the
proxy through the GA OpenAIRealtimeLLMService. Tests carry a new
realtime_e2e marker and skip cleanly when the proxy or provider creds
are absent, so they stay out of the default unit run.

* test: move realtime e2e suite into tests/e2e harness

Replace the standalone tests/realtime_e2e with a tests/e2e/realtime suite
that follows the existing e2e conventions: a session-scoped client fixture,
a frozen-dataclass RealtimeClient wrapping the shared Gateway, pydantic
models for every sent and received event, and the e2e marker with the
parent harness's liveness skip. The suite opens the proxy realtime
websocket (websockets.sync to stay synchronous like the rest of the
harness) and asserts the normalized OpenAI GA event sequence for a text
conversation plus a full tool-call round-trip, parametrized across
providers. A provider whose realtime alias is not configured on the proxy
skips via /model/info. Adds a gemini realtime model to the gateway config
and fixes the openai realtime model id.

* test: add pipecat realism layer to realtime e2e suite

Add test_realtime_pipecat_e2e driving the same providers through pipecat's
GA OpenAIRealtimeLLMService with base_url pointed at the proxy, as a coarse
realism check on top of the raw-websocket suite. Each test stays synchronous
and runs the async pipecat pipeline via asyncio.run, and the module skips
unless pipecat-ai is installed. Lift the shared provider matrix, ws-url
helper, and skip helper into realtime_client so both suites use them.

* fix(e2e): parse GA realtime transcript events in e2e client

The realtime e2e client speaks the GA protocol, but transcript() only
aggregated beta delta event names. Handle GA deltas, fall back to
response.done output, and accept nested usage details on response.done.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(e2e): address realtime code-review findings

- Use the real openai/gpt-4o-realtime-preview model ID in the gateway
  config (gpt-realtime-2 does not exist and would fail every live test)
- Pass a bare base_url to pipecat's OpenAIRealtimeLLMService so pipecat
  can append ?model= itself; the previous realtime_ws_url already
  contained ?model= causing a malformed duplicated query parameter
- Wrap connection.recv() in a try/except TimeoutError in collect_until
  so a deadline expiry inside recv preserves the collected-events
  diagnostic instead of raising a bare, message-free exception

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(e2e): filter configured_models to mode:realtime entries only

ModelInfoEntry.model_info used CustomPricing (extra="ignore") so the
mode field from /model/info was silently dropped, making it impossible
to distinguish realtime from non-realtime deployments. Add an optional
mode field to CustomPricing and filter configured_models() to entries
whose model_info.mode == "realtime" so skip_if_unconfigured never
accidentally skips a realtime test due to a naming-pattern collision
with a non-realtime deployment.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update litellm-config.yml

* fix(e2e): use TypeVar instead of PEP 695 generic in realtime parse_last

PEP 695 type-parameter syntax (def f[T: Bound](...)) is only parseable on
Python 3.12+, but the project declares requires-python >=3.10. Importing the
realtime e2e client on 3.10/3.11 raised a SyntaxError before any test could
run. Switch parse_last to the backport-safe TypeVar idiom so the suite imports
across the full supported range.

* fix(e2e/realtime): use GA openai/gpt-realtime model id

The realtime gateway config used openai/gpt-realtime-2, which is not a real
OpenAI model id and would 404 once live OpenAI realtime credentials are wired
in. The GA speech-to-speech model is openai/gpt-realtime (snapshot
gpt-realtime-2025-08-28); switch the openai-realtime alias to it.

* fix(realtime): harden Gemini/Vertex Live for audio-native e2e

Coerce TEXT responseModalities to AUDIO on native-audio and flash-live
models, suppress the orphan turnComplete response.done that arrives
immediately after tool results, omit function_response.id on Vertex,
stop appending client query params to Gemini/Vertex WSS URLs, and add
regression tests for these paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add xai full compatibility

* Add working vertex ai realtime tests

* Add audio + server vad e2e tests

* Add config for e2e testing models

* Add fix xai server vad

* fix: use correct OpenAI realtime model ID in e2e gateway config

openai/gpt-realtime is not a valid model; replace with the correct
openai/gpt-4o-realtime-preview model ID to prevent model-not-found
errors when running the openai-realtime e2e tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: restore openai/gpt-realtime model ID

gpt-realtime is a valid model; reverting the unnecessary change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve UP006 violations, mock test failures, and stale spec field

- Guard gemini setup-without-tools deferral with litellm.gemini_live_defer_setup
  flag so the default (False) path sends setup immediately, fixing two failing
  mock tests: test_client_ack_caches_setup_to_prevent_duplicate_session_update_setup
  and test_deferred_setup_sends_session_update_before_buffered_audio
- Replace deprecated typing generics (Dict, List, Tuple, Optional) with builtin
  equivalents in xai/realtime/transformation.py, gemini/realtime/transformation.py,
  and realtime_streaming.py to satisfy the UP006 ruff-strict ceiling
- Remove 'role' from OpenAPI compliance test expected fields; Google removed it
  from the Interaction schema in their live spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use Optional[dict] in xai normalizer to preserve Black line-split

dict[str, Any] | None is shorter than Optional[Dict[str, Any]] by enough
that Black collapses the _normalize_usage signature to a single line
(86 chars), conflicting with the existing multiline format. Using
Optional[dict[str, Any]] keeps the line at 90 chars (> 88 limit) so
Black preserves the multiline shape, while still satisfying UP006 by
replacing Dict with dict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove proxy-level setup-tools deferral, delegate to transformer

The _gemini_setup_deferred / _gemini_pre_setup_buffer block in
_send_to_backend was double-deferring: GeminiRealtimeConfig already
handles the session.update-to-setup mapping internally and always
returns a ready-to-send setup on the first session.update call
(session_configuration_request=None). The proxy layer was incorrectly
holding back that setup waiting for tools that the transformer had
already incorporated.

Removing the block fixes two failing tests:
  test_client_ack_caches_setup_to_prevent_duplicate_session_update_setup
  test_deferred_setup_sends_session_update_before_buffered_audio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: abstract Gemini protocol keys out of core and use cost map for live model detection

Move Gemini-specific message key knowledge (setup, realtimeInput, clientContent,
toolResponse) out of the core RealTimeStreaming module into provider-level methods.
BaseRealtimeConfig gains is_setup_message and is_content_message (both default False);
GeminiRealtimeConfig overrides them with the actual Gemini key checks.

Add gemini_native_audio and gemini_audio_only_live capability flags to the 10
affected model entries in the cost map. _is_audio_only_live_model and
_is_native_audio_model now read from the cost map first and fall back to the
existing string markers for models not in the map.

* fix: apply black formatting and register gemini capability fields in schema

* refactor: drop string-marker fallback; resolve audio-only live models via cost map only

* fix: use registered cost-map model name in vertex realtime tests

* fix: patch cost map in tests so they don't depend on remote main branch state

* fix: align gateway config vertex-realtime model ID with cost-map registered name

* fix: patch gemini-2.5-flash-native-audio in cost map fixture for CI

* fix(e2e): use correct OpenAI realtime model id in gateway config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(e2e): add budget rescheduler short intervals to gateway config

Without proxy_budget_rescheduler_min/max_time set, the rescheduler
defaults to ~600s, causing all budget-reset e2e tests to timeout
before the reset fires. Set to 5–10s so tests complete within 90s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(e2e): strip non-realtime files from PR scope

Restore budget, spend-tracking, and custom-pricing test files to their
litellm_internal_staging state. Keep the mode field addition to
CustomPricing in models.py (needed by realtime configured_models filter).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): restore async_realtime regression test and add missing fixture

- Restore the end-to-end async_realtime regression test for Vertex
  query-param forwarding; the previous unit-only version did not exercise
  the code path where the original bug lived
- Add patch_gemini_audio_cost_map_entries fixture to
  test_gemini_audio_only_live_models_drop_text_from_text_audio_combo
  so it does not depend on the cost map having gemini_audio_only_live
  set in CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): resolve ANN401 violations in realtime streaming code

Define RealtimeEventNormalizer Protocol and replace bare Any annotations
with typed alternatives (object for event/value params, the Protocol for
the normalizer) to stay within the strict-rule budget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: black format realtime_streaming.py

* fix(tests): add gemini_native_audio and gemini_audio_only_live to model prices schema

* fix(lint): fix I001 import sort order in realtime_streaming.py

* fix(lint): restore import litellm to correct position before from-litellm imports

* undo budget removal

* test(e2e): pin explicit credentials for gemini and vertex realtime models

* test(e2e): share keepalive-safe LiteLLMRealtimeLLMService across pipecat suites

The pipecat smoke test drove the proxy through the stock OpenAIRealtimeLLMService,
which sends websocket keepalive pings at its default interval. The proxy does not
answer them, so the connection is closed with a 1011 before the run completes.
Move the proxy-aware LiteLLMRealtimeLLMService (keepalive disabled) into a shared
pipecat_service module and use it from both the smoke and audio suites.

* test(e2e): document that LiteLLMRealtimeLLMService._connect keeps the ?model= param

The proxy routes realtime websockets on the ?model= query param, and pipecat's
OpenAIRealtimeLLMService.__init__ bakes it into self.base_url before _connect
runs. Passing self.base_url through preserves it; spell that out so the override
is not misread as dropping the param.

* fix(realtime): set _content_sent_after_setup only after the backend send succeeds

A failed content send used to flip _content_sent_after_setup to True before the
send was confirmed, mirroring the correct-on-failure ordering the adjacent
session-config cache already follows. If the send raised, the flag stayed True
and a later session.update that produced a setup frame was silently dropped even
though the backend never received any content. Set the flag after the send
succeeds and add a regression test that fails if the ordering is reverted.

* fix: normalize realtime passthrough events

* refactor(realtime): declare patch_outgoing_session on normalizer Protocol; fix wav chunk return type

The RealtimeEventNormalizer Protocol only declared should_drop and normalize,
so the outgoing session.update patch went through a getattr(..., None) lookup
even though should_drop/normalize are called directly. The sole implementer
(XAIRealtimeNormalizer) already provides patch_outgoing_session, so declare it
on the Protocol and call it directly for consistent, fully-typed dispatch.

Also correct _load_wav_chunks' return annotation from list[bytes] to
tuple[list[bytes], int]; it returns (chunks, sample_rate) and the caller
unpacks both.

---------

Co-authored-by: mubashir1osmani <mubashir.osmani777@gmail.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 09:36:49 -07:00
..
budgets test(e2e): drop xfail markers for the now-fixed team-budget-JSON and custom-pricing-leak bugs (#31249) 2026-06-24 18:51:10 -07:00
gateway test: add realtime proxy e2e suite across providers (#30960) 2026-06-26 09:36:49 -07:00
llm_translation test(e2e): drop xfail markers for the now-fixed team-budget-JSON and custom-pricing-leak bugs (#31249) 2026-06-24 18:51:10 -07:00
realtime test: add realtime proxy e2e suite across providers (#30960) 2026-06-26 09:36:49 -07:00
spend_tracking test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
conftest.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
e2e_config.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
e2e_gateway.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
e2e_http.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
lifecycle.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
models.py test: add realtime proxy e2e suite across providers (#30960) 2026-06-26 09:36:49 -07:00
pytest.ini test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
test_lifecycle.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00
transport.py test: add e2e tests for spend, budgets and llms (#30869) 2026-06-24 15:01:57 -07:00