test(litellm): parametrize sync/async test pairs - #739
Conversation
Collapse 13 duplicated sync/async LiteLLM test pairs into single parametrized tests using a shared `sync_async` marker plus two small helpers (`_call` and `_collect_stream`). Cuts test_litellm.py from 1088 to ~828 lines with no loss of coverage. Where the async variant historically asserted less than its sync twin, the unified body now applies the stronger assertions to both (verified against the existing cassettes): streaming content/output checks, rerank index/output-shape/search_units, and the litellm-auto span origin. Cassettes are renamed to the parametrized node names (e.g. test_litellm_acompletion_metrics.yaml -> test_litellm_completion_metrics[async].yaml) across both the latest and 1.74.0 matrix dirs. Pure git renames, no recorded content changed. Validated: nox -s "test_litellm(latest)" and "test_litellm(1.74.0)" both pass 38 tests; pylint (--errors-only) and pre-commit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ecf1a5954
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".
| if is_async: | ||
| return await async_fn(*args, **kwargs) | ||
| return sync_fn(*args, **kwargs) |
There was a problem hiding this comment.
Run sync cases outside the asyncio loop
For every is_async=False parameter, sync_fn is now invoked from inside a coroutine managed by pytest-asyncio, so these tests no longer exercise LiteLLM's synchronous entry points in the no-running-event-loop context used by ordinary synchronous callers. LiteLLM and its transports can select loop-dependent execution paths, meaning this refactor can hide sync-only failures even though both parameter values pass; keep a synchronous test driver for the sync variants while sharing only the call arguments and assertions.
Useful? React with 👍 / 👎.
Address /simplify review feedback: - _collect_stream now uses (async) comprehensions instead of a manual append loop. - test_litellm_image_generation sends the blue prompt on the async variant, matching the recorded [async] cassette body (the sync variant keeps red) so code and cassettes agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses PR review (P2): the parametrized tests were `async def` + @pytest.mark.asyncio, so the `is_async=False` variants invoked LiteLLM's sync entrypoints from inside a running event loop -- not how ordinary synchronous callers use them. LiteLLM and its transports can select loop-dependent code paths, so sync-only failures could be hidden even though both parameter values pass. The parametrized tests are now plain `def` functions. `_run` calls the sync entrypoint directly (no running loop) and drives the async entrypoint with `asyncio.run`; `_run_stream` does the same for streaming (await + async-drain under asyncio.run). The genuinely async-only tests (async_parallel_requests, async_streaming_with_break) keep @pytest.mark.asyncio. Validated: nox test_litellm(latest) and (1.74.0) both pass 38; pylint and pre-commit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Collapses 13 duplicated sync/async LiteLLM test pairs into single parametrized tests, addressing the ~15 mechanical duplicate pairs in
test_litellm.py. The file shrinks from 1088 → 828 lines (−242 net) with no loss of coverage.A shared marker plus two small helpers drive the dispatch:
Parametrized pairs: completion, text_completion, completion_streaming, responses, embedding, moderation, image_generation, completion_with_system_prompt, transcription, speech, completion_error, responses_streaming, rerank.
Genuinely-single tests (prompt_caching, async_parallel_requests, tool_calls, async_streaming_with_break, the boolean/rerank unit tests, the
patch_litellm_*subprocess tests, and auto-instrument) are left untouched.Small coverage gains
Where an async variant historically asserted less than its sync twin, the unified body applies the stronger set to both — verified against the existing cassettes first:
"24"in the async case toorerank[async]now checksresults[0]["index"] == 0, output-key shape, andsearch_unitscompletion_metrics[async]now checks thelitellm-autospan originCassettes
Because pytest-recording names cassettes after the test node, the 24 paired cassettes were renamed to their parametrized names (e.g.
test_litellm_acompletion_metrics.yaml→test_litellm_completion_metrics[async].yaml) across both thelatest/and1.74.0/matrix dirs — 48 puregit mvrenames, no recorded content changed.Validation
nox -s "test_litellm(latest)"→ 38 passed (matches pre-change baseline)nox -s "test_litellm(1.74.0)"→ 38 passedmake pylint(--errors-only) → clean; pre-commit (ruff-format, ruff check, codespell) → cleanmake check-stale-cassettes→ no stale dirs🤖 Generated with Claude Code
Created by Abhijeet Prasad (@AbhiPrasad)
Slack thread