fix(vertex_ai): stabilize nullable tool schema request bodies and skip live test on 429 - #32430
Conversation
…p live test on 429
Greptile SummaryThis PR fixes an intermittent CI failure caused by Python hash-seed non-determinism in the Vertex AI schema serialization path, and adds a skip guard so residual live-API rate-limit errors don't redden unrelated PRs.
Confidence Score: 5/5Safe to merge — a minimal, well-evidenced fix for a CI flake with no production behavior change for correct inputs. The one-line set-to-tuple conversion is narrowly scoped to iteration order inside No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/vertex_ai/common_utils.py | Changes type_specific_fields from a set literal to a tuple, ensuring deterministic insertion order when building the anyOf branch so JSON key order is stable across Python hash seeds. |
| tests/local_testing/test_amazing_vertex_completion.py | Wraps the live litellm.completion call in test_gemini_nullable_object_tool_schema_httpx with a try/except to skip on RateLimitError, matching the pattern used by 22 other call sites in the same file. |
| tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py | Adds a regression test that spawns subprocesses under PYTHONHASHSEED 0–5 and asserts all serialized _build_vertex_schema outputs are byte-identical; confirms the fix deterministically and fails on the pre-fix code. |
Reviews (1): Last reviewed commit: "fix(vertex_ai): stabilize nullable tool ..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Relevant issues
No GitHub issue; fixes the
local_testing_part1CI flake that turns unrelated PRs red, e.g. job 2010804 on #32390Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito 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
Proofs for branch
litellm_fix_vertex_429_flake_local_testing. Before runs captured at base commit 06a43d1, after runs at fix commit 8ac1b05Before (CI): job 2010804 (
local_testing_part1on #32390, whose diff only touches unrelated realtime test harness code) fails on exactly one test,test_gemini_nullable_object_tool_schema_httpx, withlitellm.RateLimitError ... 429 RESOURCE_EXHAUSTEDfrom a livegemini-2.5-flash:generateContentcall. The job's VCR verdict for the test is[VCR NOOP] played=0 entries=1and its VCR diagnostic log shows why the cached episode was not replayed: the incoming and cached request bodies are both 807 bytes and first diverge at byte 404, whererequiredandpropertiesswap order insidecustomer_context.anyOf[0]Before (local, at 06a43d1): the serialized request schema depends on the interpreter hash seed, producing two distinct bodies, which is exactly what makes the cassette miss and the call go live
After (local, at 8ac1b05): every seed produces identical bytes, and the surviving ordering is the one the majority of seeds already recorded, so existing cassettes keep replaying
After (live end-to-end, at 8ac1b05): real Vertex AI call, no mocks, real spend,
litellm.completion(model="vertex_ai/gemini-2.5-flash", tools=<the exact flaking tool schema>, tool_choice="required")Supplementary check of the new skip guard (quota exhaustion cannot be triggered on demand, so this one simulates the 429 at the HTTP transport; it verifies the test plumbing only). Running
pytest tests/local_testing/test_amazing_vertex_completion.py::test_gemini_nullable_object_tool_schema_httpxwith the same forced 429:New regression test fails on the pre-fix code and passes with the fix, and the touched suites are green at 8ac1b05:
Type
🐛 Bug Fix
Changes
CircleCI job
local_testing_part1intermittently fails unrelated PRs with a Vertex AI 429 ontest_gemini_nullable_object_tool_schema_httpx(observed on #32390, job 2010804; in a sample of 600build_and_testworkflows from 2026-07-03 to 2026-07-08 this was 1 of 16local_testing_part1failures)Root cause:
_convert_schema_typesinlitellm/llms/vertex_ai/common_utils.pyconverts"type": ["object", "null"]tool params toanyOfand movesproperties,requiredetc. into the object branch by iterating a Pythonset. String set iteration order varies per process under hash randomization, so the JSON key order of the serialized request body differs from run to run; this test's schema produces exactly two orderings. The local_testing VCR harness matches requests as canonical bytes and deliberately does not normalize JSON key order, so any CI worker whose seed produces the ordering that is not in the Redis cassette misses and calls the live API. That call draws on the shared CI project quota (this job alone is 4 CircleCI nodes xpytest -n 4, the file has 20gemini-2.5-flashcall sites, and all concurrent PRs share the project), so it sometimes gets a 429. The test had no guard for that: the file-levellitellm.num_retries = 3is intentionally reset to theNonedefault by the conftest isolation fixture, and unlike 22 sibling call sites in the same file there was noexcept litellm.RateLimitError: pytest.skiphandler, so a single 429 turns an unrelated PR red. The failure then blocks the cassette save (the persister saves only on pass, and non-2xx responses are filtered from recording anyway), so nothing self-heals within the day, and the 24h cassette TTL restarts the record cycle dailyFix, targeting two links of that chain.
type_specific_fieldsbecomes a tuple, so theanyOfbranch is built in a fixed order and the request body is byte-identical across processes; after the first recording the cassette replays for every run regardless of seed, and only the intentional once-per-TTL re-record stays live. The test also adopts the file's standard skip-on-RateLimitError guard so that residual live call cannot fail an unrelated PR when quota happens to be exhausted. A new regression test builds the exact flaking schema through_build_vertex_schemain subprocesses underPYTHONHASHSEED0-5 and asserts all serialized outputs are byte-identical; it fails deterministically on the pre-fix code and passes with the fix