Skip to content

test(e2e): replace custom endpoints_client with provider SDK clients - #34358

Open
mateo-berri wants to merge 2 commits into
litellm_internal_stagingfrom
claude/e2e-tests-custom-endpoints-qxoi1o
Open

test(e2e): replace custom endpoints_client with provider SDK clients#34358
mateo-berri wants to merge 2 commits into
litellm_internal_stagingfrom
claude/e2e-tests-custom-endpoints-qxoi1o

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • e2e LLM tests drove endpoints through a bespoke endpoints_client.py wrapper
  • no customer runs that wrapper, so tests missed the real client contract

How it solves it:

  • tests now use the real OpenAI and Anthropic SDKs pointed at the proxy
  • endpoints_client.py is deleted; a small sdk fixture builds the SDK clients
  • rerank (no official SDK) moves onto ProxyClient on the shared typed transport

Relevant issues

Linear ticket

Resolves LIT-4577

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

The change under test is the client itself: the point of LIT-4577 is that customers reach the proxy through the OpenAI and Anthropic SDKs, so curl cannot exercise what changed here. The migrated tests are exactly the customer flow (SDK client, virtual key, live proxy, real provider APIs, real spend), so the proof is a live run of them at commit 76bf0cd against a proxy started with store_model_in_db: true, Postgres, Redis, and real provider keys

$ .venv/bin/python litellm/proxy/proxy_cli.py --config e2e_proxy_config.yaml --port 4000
$ pytest tests/e2e/llm_translation/test_messages_e2e.py \
    tests/e2e/llm_translation/test_credential_messages_e2e.py \
    "tests/e2e/llm_translation/test_responses_e2e.py::TestResponses::test_responses_anthropic_returns_completion" \
    "tests/e2e/llm_translation/test_responses_e2e.py::TestResponses::test_responses_anthropic_returns_function_call" \
    tests/e2e/llm_translation/test_responses_metadata_e2e.py \
    tests/e2e/llm_translation/test_custom_pricing_e2e.py -v

tests/e2e/llm_translation/test_messages_e2e.py::TestAnthropicMessages::test_messages_returns_completion PASSED [  9%]
tests/e2e/llm_translation/test_messages_e2e.py::TestAnthropicMessages::test_messages_logs_cost_matching_the_response_header PASSED [ 18%]
tests/e2e/llm_translation/test_messages_e2e.py::TestAnthropicMessages::test_messages_streams_completion PASSED [ 27%]
tests/e2e/llm_translation/test_messages_e2e.py::TestAnthropicMessages::test_messages_tool_use PASSED [ 36%]
tests/e2e/llm_translation/test_credential_messages_e2e.py::TestCredentialBackedMessages::test_credential_backed_messages PASSED [ 45%]
tests/e2e/llm_translation/test_responses_e2e.py::TestResponses::test_responses_anthropic_returns_completion PASSED [ 54%]
tests/e2e/llm_translation/test_responses_e2e.py::TestResponses::test_responses_anthropic_returns_function_call PASSED [ 63%]
tests/e2e/llm_translation/test_responses_metadata_e2e.py::TestResponsesMetadata::test_store_metadata_continues_and_redis_keys_have_ttl PASSED [ 72%]
tests/e2e/llm_translation/test_custom_pricing_e2e.py::TestCustomPricing::test_custom_pricing_is_billed_at_configured_rate PASSED [ 81%]
tests/e2e/llm_translation/test_custom_pricing_e2e.py::TestCustomPricing::test_model_info_reports_custom_pricing PASSED [ 90%]
tests/e2e/llm_translation/test_custom_pricing_e2e.py::TestCustomPricing::test_custom_pricing_is_isolated_from_sibling_deployment PASSED [100%]

============================== 11 passed in 21.09s ==============================

Those 11 tests cost real Anthropic and Gemini money and cover both SDK clients end to end: OpenAI SDK nonstream, tool calling, and store/metadata/previous_response_id on /v1/responses, and Anthropic SDK nonstream, raw-response cost header, SSE streaming, and tool use on /v1/messages, with spend-log read-back and Redis TTL checks intact

The remaining cells could not be exercised from this sandbox for credential reasons only, all surfaced correctly as typed SDK errors: the OpenAI key returns insufficient_quota, the AWS token is invalid for Bedrock, the Azure AI resource has no claude-haiku-4-5 deployment, and there is no Cohere key, Vertex service account, or enterprise license (needed by allowed_passthrough_routes in the pre-existing passthrough-headers test). They need one QA pass from an environment with full keys; the QA runbook below covers them

Gates run locally at the same commit: make lint-e2e-basedpyright reports 0 errors, check_e2e_no_raw_requests passes, the full suite collects 87 tests, and python -m coverage_registry.collector --strict accepts every covers marker

Type

🧹 Refactoring
✅ Test

Changes

tests/e2e/llm_translation/sdk_clients.py is new: a frozen SdkClients dataclass whose openai(key) / anthropic(key) builders return real SDK clients pointed at LITELLM_PROXY_URL with the harness request timeout and retries disabled, exposed as the session-scoped sdk fixture in the suite conftest. Failures now surface as the SDK's own typed exceptions, which is what an end user sees

Migrated to the OpenAI SDK: test_responses_e2e.py (create, stream events, with_raw_response for the x-litellm-call-id header, typed FunctionToolParam tools, vision input parts), test_responses_metadata_e2e.py (store, metadata, previous_response_id), test_embeddings_endpoint_e2e.py, test_audio_speech_e2e.py (with_raw_response and with_streaming_response for the chunked-transfer assertions), test_audio_transcriptions_e2e.py, test_moderations_e2e.py, test_image_generation_e2e.py

Migrated to the Anthropic SDK: test_messages_e2e.py (create, with_raw_response for the x-litellm-response-cost header, stream=True raw events, ToolParam tools), test_messages_azure_foundry_e2e.py (including the x-litellm-rust header check on a raw streamed response), test_credential_messages_e2e.py, and both mid-conversation-system files. The mid-conversation files cast the role: "system" reminder turn to MessageParam because the SDK type only admits user/assistant while that wire shape is exactly what is under test; the SDK serializes the dict verbatim

Rerank has no official OpenAI/Anthropic SDK, so ProxyClient.rerank plus RerankBody/RerankResponse in models.py replace the wrapper method and test_rerank_e2e.py uses them. test_ocr_rust_e2e.py, test_custom_pricing_e2e.py, test_cache_control.py, and test_passthrough_headers_e2e.py only used the wrapper for model registration or shared pydantic models, so they now use the proxy fixture and models.py imports directly and their assertions are unchanged

endpoints_client.py is deleted. anthropic==0.84.0 joins the e2e-dev dependency group (matching the ci group pin) so the lint CI env resolves the new imports; openai is already a main dependency. tests/e2e/CLAUDE.md and CONTRIBUTING.md document the SDK exception to the shared-transport rule. Follow-up commit 0e34c18 updates the documented suite run commands in tests/e2e/CONTRIBUTING.md to name the e2e-dev group (uv run --group e2e-dev pytest ...) so a default dev-group environment collects the suite

QA runbook

Environment prerequisites: a proxy with store_model_in_db: true, Postgres, Redis, and keys for OpenAI, Anthropic, AWS Bedrock, Azure AI (with a claude-haiku-4-5 deployment), Vertex, Cohere, and Mistral depending on the cell. Every test registers its own deployment through /model/new and deletes it on teardown, so no static model config is needed. In the snippets below, key is a fresh virtual key from /key/generate and both SDKs are constructed with base_url="http://localhost:4000" and the virtual key as api_key

  • tests/e2e/llm_translation/test_responses_e2e.py (TestResponses: nonstream, stream, cost_logged, tool_use, vision for openai; nonstream and tool_use for anthropic and bedrock backends) - the OpenAI SDK Responses client works against the proxy exactly as against OpenAI
    • OpenAI(base_url="http://localhost:4000", api_key=key).responses.create(model=m, input="reply with one word", instructions="You are a helpful assistant") returns non-empty output_text
    • Same call with stream=True yields response.output_text.delta events and ends with response.completed
    • With tools=[get_weather function tool] the output contains a function_call item whose arguments parse to a location
    • With an image input part, the answer names the animal in the picture
    • For the cost cell, the raw response carries x-litellm-call-id and polling /spend/logs by the response id lands a row with spend > 0 for gpt-4o-mini
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_responses_metadata_e2e.py::TestResponsesMetadata::test_store_metadata_continues_and_redis_keys_have_ttl - store + metadata sessions continue via previous_response_id and leave no unbounded Redis keys
    • Call responses.create with store=True and a unique metadata.session_id, then a second call with previous_response_id set to the first id; both return text
    • redis-cli --scan --pattern "*<marker>*" then TTL on each key; expect no TTL of -1
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_messages_e2e.py (TestAnthropicMessages: nonstream, cost_logged, stream, tool_use) - the Anthropic SDK Messages client works against the proxy end to end
    • Anthropic(base_url="http://localhost:4000", api_key=key).messages.create(model=m, max_tokens=64, messages=[{"role": "user", "content": "reply with one word"}]) returns an assistant message with text
    • messages.with_raw_response.create(...) carries a positive x-litellm-response-cost header, and the priced /spend/logs row for the key matches it within 1% with non-zero token counts
    • messages.create(..., stream=True) yields content_block_delta events and reaches message_stop
    • With a get_weather tool the response content contains a tool_use block
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_messages_azure_foundry_e2e.py (TestAzureFoundryMessages: basic and tool_use, nonstream and stream) - azure_ai Claude deployments serve the Anthropic SDK contract
    • Repeat the basic and tool-use calls above against an azure_ai/claude-haiku-4-5 deployment; streamed tool use must carry a content_block_start with a tool_use block
    • With E2E_EXPECT_RUST=1, the streamed raw response must carry x-litellm-rust: true
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_credential_messages_e2e.py::TestCredentialBackedMessages::test_credential_backed_messages - a stored credential resolves into a servable deployment
    • POST /credentials with the master key and an Anthropic api_key, POST /model/new referencing litellm_credential_name, then the Anthropic SDK basic call returns assistant text
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_messages_mid_conversation_system_e2e.py and test_messages_mid_conversation_system_native_providers_e2e.py (flagged keeps cache, unflagged hoists; bedrock invoke, azure_ai, vertex) - mid-conversation role "system" handling is model-aware
    • Prime the cache: Anthropic SDK call with a >1024-token system block marked cache_control and a cache-marked user turn, repeated until usage shows both cache_read and cache_creation tokens > 0
    • Send the same conversation plus a {"role": "system"} reminder turn, an assistant turn, and a fresh user turn; on a flagged model (e.g. claude-sonnet-5 or claude-opus-4-8) usage.cache_read_input_tokens must be at least the full primed prefix
    • On an unflagged model (e.g. claude-haiku-4-5 invoke or claude-opus-4-7) the same shape must return a completion instead of a provider 400
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_embeddings_endpoint_e2e.py (openai, bedrock, vertex vectors) - /embeddings returns a real vector through the OpenAI SDK
    • embeddings.create(model=m, input="Say this is a test!") returns a non-empty vector with at least one non-zero component for each of the three backends
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_audio_speech_e2e.py (nonstream and stream) - /v1/audio/speech returns audio and actually streams
    • audio.speech.with_raw_response.create(model=m, voice="alloy", input="Hello!") has an audio content-type and non-empty bytes
    • audio.speech.with_streaming_response.create(...) with a longer input shows transfer-encoding: chunked, no content-length, and non-zero streamed bytes
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_audio_transcriptions_e2e.py::TestAudioTranscriptions::test_audio_transcriptions_returns_text - a spoken weather question transcribes to text mentioning weather
    • audio.transcriptions.create(model=m, file=(name, wav_bytes, "audio/wav")) with the realtime suite's weather WAV returns a transcript containing "weather"
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_moderations_e2e.py (violent flagged, benign passes) - /v1/moderations classifies both sides of the decision
    • moderations.create(model=m, input=<violent text>) returns flagged=true with at least one true category; the benign sentence returns flagged=false
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_image_generation_e2e.py (openai, bedrock) - /v1/images/generations returns an image through the OpenAI SDK
    • images.generate(model=m, prompt="Draw a cute cat", n=1, size="1024x1024") returns data with a url or b64_json for both backends
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_rerank_e2e.py (cohere, bedrock) - /v1/rerank scores documents within top_n, now via ProxyClient.rerank on the shared transport
    • curl -X POST http://localhost:4000/v1/rerank -H "Authorization: Bearer $KEY" -d '{"model": m, "query": "What is the capital of the United States?", "documents": [...4 docs...], "top_n": 3}' returns at most 3 results with a relevance_score on the top hit
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/llm_translation/test_ocr_rust_e2e.py, test_custom_pricing_e2e.py, test_cache_control.py, test_passthrough_headers_e2e.py - harness rewiring only (proxy fixture and models.py imports replace the deleted wrapper); request shapes and assertions are byte-for-byte the previous behavior, so the existing runbooks for those tests still apply. The custom pricing trio passed live in this run as shown above
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

The llm_translation suite drove /v1/responses, /v1/messages, /embeddings,
/v1/images/generations, /v1/moderations and /v1/audio/* through a bespoke
endpoints_client wrapper that no customer runs. Tests now call the proxy the
way customers do: the OpenAI SDK for the OpenAI-compatible surface and the
Anthropic SDK for /v1/messages, wired through a session-scoped sdk fixture
(sdk_clients.py) that points both SDKs at the proxy with a virtual key.

Endpoints no official SDK covers keep the shared typed transport: rerank moves
onto ProxyClient (RerankBody/RerankResponse in models.py) and the passthrough
header test parses with the shared AnthropicMessagesResponse model. Files that
only used endpoints_client for model registration now use the proxy fixture
directly. endpoints_client.py is deleted; anthropic joins the e2e-dev
dependency group so the lint env resolves the SDK imports.

Resolves LIT-4577
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing claude/e2e-tests-custom-endpoints-qxoi1o (0e34c18) with litellm_internal_staging (86eee8b)

Open in CodSpeed

@mateo-berri
mateo-berri marked this pull request as ready for review July 23, 2026 06:28
@mateo-berri
mateo-berri requested a review from a team July 23, 2026 06:28
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the bespoke endpoint test client with provider SDK clients and the shared typed proxy transport.

  • Adds session-scoped OpenAI and Anthropic SDK client builders configured for the live proxy.
  • Migrates LLM translation tests to provider SDK request, response, streaming, and exception contracts.
  • Moves rerank coverage to ProxyClient with typed request and response models.
  • Adds the Anthropic SDK to the e2e-dev dependency group and updates harness documentation and commands.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains in the changes eligible for this follow-up review.

Important Files Changed

Filename Overview
tests/e2e/llm_translation/sdk_clients.py Adds proxy-configured OpenAI and Anthropic SDK factories with retries disabled and the shared request timeout.
tests/e2e/llm_translation/conftest.py Replaces the deleted endpoint-client fixture with a session-scoped SDK client fixture.
tests/e2e/proxy_client.py Adds rerank support to the shared typed proxy client.
tests/e2e/models.py Adds typed rerank request and response models used by the shared transport.
pyproject.toml Adds the pinned Anthropic SDK to the e2e development dependency group.
tests/e2e/CONTRIBUTING.md Updates local test commands to install the e2e dependency group and documents provider-SDK use in the harness.
tests/e2e/CLAUDE.md Records the provider-SDK exception to the shared-transport convention for LLM endpoint tests.

Reviews (2): Last reviewed commit: "docs(e2e): name the e2e-dev group in the..." | Re-trigger Greptile

Comment thread tests/e2e/CLAUDE.md
Comment thread tests/e2e/llm_translation/sdk_clients.py
The llm_translation suite needs the e2e-dev dependency group at collection
time (websockets for the realtime folder, now also the anthropic SDK for the
sdk fixture). make bootstrap installs the group, but the documented pytest
command did not name it, so a default dev-group environment failed collection.
Naming the group on uv run makes the command work from any environment state
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

CI note: frontend-lint is red on the merge ref, but the failure comes from the base branch, not this diff. The three flagged files (models-and-endpoints/layout.tsx, llm-credentials/page.tsx, vertexCredentialsUpload.ts) were added on litellm_internal_staging by #34327 with antd imports, and the antd no-restricted-imports rule landed separately in #34341, so the base tip fails frontend-lint by itself. This PR changes no files under ui/ (the previous run at 76bf0cd passed frontend-lint before the base moved). Will update the branch and let CI re-run once the base is green again


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant