litellm oss staging 04/13/2026 - #25665
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis staging PR bundles several independent bug fixes: the headline fix assigns correct sequential indices in Gemini batch embedding responses (replacing the hardcoded The HiddenLayer V2 integration ( Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py | Core fix: replaced hardcoded index=0 with enumerate so each embedding in a batch response receives its positional index. Straightforward and correct. |
| litellm/caching/in_memory_cache.py | Adds heap-based TTL tracking (expiration_heap) and calls evict_cache() unconditionally at the start of every set_cache() to prune stale heap entries. Step 2 of evict_cache() pops from the heap with no guard for exhaustion; safe in normal operation but a defensive check (and self.expiration_heap) would prevent a potential IndexError if the heap ever gets out of sync. |
| litellm/integrations/datadog/datadog.py | Refactors async_send_batch to drain safely: copies the queue, clears it, restores on 413 or any exception, and only stamps last_flush_time after a confirmed successful flush. Logic is sound. |
| litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py | Adds HiddenlayerGuardrailV2 class targeting detection/v2/ endpoints. Several structural issues noted in previous review threads remain unresolved in this file (structured_messages type mismatch, empty-choices IndexError). |
| litellm/types/proxy/guardrails/guardrail_hooks/hiddenlayer.py | Adds version: Optional[int] = Field(default=2, ...) to HiddenlayerGuardrailConfigModel. The default of 2 routes all existing deployments that omit the field to V2 endpoints; backwards-compatibility concern noted in previous review thread remains unresolved. |
| litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py | Refactors to a chunk_queue (deque) buffering approach and adds emission of input_json_delta when tool arguments are bundled in the same streaming chunk as the function name. Logic is well-structured. |
| litellm/router_strategy/lowest_latency.py | Changes the latency-list trim from removing the newest entry to removing the oldest entry ([1:] + [final_value]), which is the correct sliding-window behaviour. Change is minimal and correct. |
| tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py | Adds test_batch_embeddings_response_has_correct_indices_and_order as a pure unit test for the core index fix. All tests are mock-based. Test is placed in tests/litellm/ (local testing folder) rather than tests/test_litellm/ as required by CLAUDE.md for new unit tests. |
| litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/init.py | Wires HiddenlayerGuardrailV2 into the initializer registry; routes to V1 when not version or version < 2, V2 otherwise. Inherits the default-version=2 issue from the config model. |
| tests/test_litellm/caching/test_in_memory_cache.py | Adds tests for heap-bounded behaviour, eviction order, expired-first eviction, and the new prune-below-capacity fix. Tests are thorough and correctly placed in tests/test_litellm/. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[set_cache called] --> B{max_size == 0?}
B -- Yes --> Z[Return early]
B -- No --> C[call evict_cache]
subgraph EC ["evict_cache - Step 1: prune expired heap roots"]
S1A[peek heap top] --> S1B{Heap empty?}
S1B -- Yes --> S1E[exit step 1]
S1B -- No --> S1C{Entry outdated?\nexpiry != ttl_dict}
S1C -- Yes --> S1D[pop stale entry] --> S1A
S1C -- No --> S1F{Entry expired?}
S1F -- Yes --> S1G[pop + remove key] --> S1A
S1F -- No --> S1E
end
subgraph EC2 ["evict_cache - Step 2: evict if full"]
S2A{cache size >= max?} -- No --> S2E[done]
S2A -- Yes --> S2B[heappop from heap]
S2B --> S2C{Matches ttl_dict?}
S2C -- Yes --> S2D[remove key from cache] --> S2A
S2C -- No --> S2A
end
C --> EC
EC --> EC2
EC2 --> E{Value size ok?}
E -- No --> Z2[Skip insert]
E -- Yes --> F[store value in cache_dict]
F --> G{allow_ttl_override?}
G -- No --> Z3[Done - TTL preserved]
G -- Yes --> H[update ttl_dict and heappush]
Reviews (7): Last reviewed commit: "Fix mypy issues" | Re-trigger Greptile
|
|
| description="The Hiddenlayer Secret Key for the Hiddenlayer API.. If not provided, the `HIDDENLAYER_CLIENT_SECRET` environment variable is checked.", | ||
| ) | ||
|
|
||
| version: Optional[int] = Field(default=2, description="Hiddenlayer guardrail version to use.") |
There was a problem hiding this comment.
Default
version=2 silently breaks existing HiddenLayer integrations
version defaults to 2, so every existing deployment that omits the field will have LitellmParams.version == 2. In initialize_guardrail, the condition if not version or version < 2 evaluates to False, routing all existing configs to HiddenlayerGuardrailV2 and its new detection/v2/ endpoints. Users who are running a self-hosted or older SaaS HiddenLayer instance without V2 support will get unexpected 404/401 errors and their guardrails will silently stop blocking.
The default should be None (or 1) to preserve existing behaviour and make V2 an explicit opt-in.
| version: Optional[int] = Field(default=2, description="Hiddenlayer guardrail version to use.") | |
| version: Optional[int] = Field(default=None, description="Hiddenlayer guardrail version to use. Defaults to 1 (v1 API). Set to 2 to use the v2 API.") |
Rule Used: What: avoid backwards-incompatible changes without... (source)
| elif input_type == "response" and inputs.get("texts"): | ||
| inputs["texts"] = [ | ||
| output.get("choices", [{}])[-1].get("message", {}).get("content", "") | ||
| ] |
There was a problem hiding this comment.
IndexError when HiddenLayer returns an empty choices array
output.get("choices", [{}])[-1] uses [{}] as a default only when the choices key is absent. If HiddenLayer V2 returns {"choices": []} (present but empty), the expression becomes [][-1] and raises IndexError, crashing the guardrail post-call hook uncaught.
| elif input_type == "response" and inputs.get("texts"): | |
| inputs["texts"] = [ | |
| output.get("choices", [{}])[-1].get("message", {}).get("content", "") | |
| ] | |
| elif input_type == "response" and inputs.get("texts"): | |
| inputs["texts"] = [ | |
| (output.get("choices") or [{}])[-1].get("message", {}).get("content", "") | |
| ] |
Using or [{}] covers both the missing-key and empty-list cases.
### Background The Gemini batchEmbedContents response handler hardcoded `index=0` for every embedding in the response. Any consumer relying on the OpenAI-format `index` field to match embeddings back to inputs would silently get wrong associations. ### Changes Use `enumerate` in `process_response` so each embedding gets its positional index instead of 0. ### Test Plan Added unit test asserting sequential indices and correct vector ordering for a 3-element batch response.
…hunk (#25533) * fix: emit input_json_delta for tool args bundled in first streaming chunk Some providers (xAI, Gemini) include tool_call function arguments in the same streaming chunk as the function name/id. The AnthropicStreamWrapper was discarding the trigger chunk entirely when starting a new content block, which silently dropped the input_json_delta carrying tool arguments. This caused tool_use blocks to arrive with empty input {}. Now queue the processed_chunk after content_block_start when it carries non-empty input_json_delta data. Backward compatible: providers that send empty arguments in the first chunk (OpenAI-style) are unaffected since the condition checks for truthy partial_json. * test: add tests for input_json_delta emission on bundled tool args Covers the fix for providers (xAI, Gemini) that bundle tool_call arguments in the same streaming chunk as the function name/id. Verifies the AnthropicStreamWrapper emits input_json_delta after content_block_start, and that empty-arg chunks (OpenAI-style) are unaffected. * style: apply Black formatting to streaming_iterator.py * fix: mirror input_json_delta fix to sync __next__ and add sync tests * test: make no_extra_delta tests assert explicitly instead of passing silently
…onfigured have no budget enforcement (#25557) * fix #25506 * address greptile review feedback * [Test] UI - Models: Add E2E tests for Add Model flow Add E2E tests covering: - Test connection with bad credentials shows failure modal - Adding a specific model and verifying it appears in All Models table - Adding a wildcard route and verifying it appears in All Models table - Verifying model dropdown shows provider-specific models (existing test updated) Added data-testid attributes to UI components to support stable test selectors. Tests verified passing 3/3 consecutive runs with zero flakiness. * address greptile review feedback (greploop iteration 1) Add cleanup helper to delete models created during tests, preventing stale data accumulation across repeated test runs. * fix CI: replace data-testid selectors with text/role-based selectors The data-testid attributes added to React components are not present in the CI-built UI output. Switch to using getByRole and getByText selectors which work with the rendered DOM regardless of build cache. * remove unnecessary cleanup helper The database is freshly seeded on every test run via seed.sql, so per-test cleanup is not needed. --------- Co-authored-by: Yuneng Jiang <yuneng@berri.ai> Co-authored-by: Krrish Dholakia <krrish+github@berri.ai>
* Serialize error message to a string; only scan last message * Update litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Add v2 of hiddenlayer guardrail implementation * Update litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Fix potential header issue * linting * Add image support --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…t_latency strategy (#25548) * fix(router): discard oldest entry when trimming latency list in lowest_latency strategy The lowest_latency routing strategy keeps a rolling window of the most recent latency and time-to-first-token measurements per deployment. When the window is full, the strategy was discarding the *newest* value instead of the oldest, because the trim used `[: max_latency_list_size - 1]` (keeping indices 0..N-2) rather than `[1:]` (dropping index 0 and keeping indices 1..N-1). Since new values are appended at the end, the bug meant the most recent measurement was always dropped once the list reached capacity. The routing decisions then relied on stale data (including any early-spike values that never aged out), and timeout penalties written via `async_log_failure_event` were silently discarded as well. Fix the slice in all five call sites (sync + async log_success_event for both latency and time_to_first_token, and async_log_failure_event for the timeout penalty) and add regression tests covering each path. * test(router): cover async TTFT trim path in lowest_latency regression tests Adds test_ttft_list_trimming_discards_oldest_entry_async, an async counterpart to test_ttft_list_trimming_discards_oldest_entry that drives async_log_success_event with a ModelResponse and completion_start_time so the async time_to_first_token trim branch is actually exercised. Previously no test touched that code path: the sync TTFT test used log_success_event, and the async latency test passed a plain dict response_obj without stream/completion_start_time, so TTFT was never computed and the async trim was unreached. Verified load-bearing by reverting only the async TTFT slice — the new test fails and all others pass. * format
* fix: drain datadog batches safely * fix: preserve datadog batches on 413 * fix: import time in datadog flush queue * test: cover datadog batching edge cases * fix: only stamp successful datadog flushes * test: use sync mock for datadog payload builder
…ns (#23337) Vertex AI rejects requests containing both search tools (googleSearch, enterpriseWebSearch, urlContext) and function declarations with error: 'Multiple tools are supported only when they are all search tools.' When _merge_tools_from_deployment() combines deployment-level search tools with user-request function tools (e.g. via MCP), the mixed tool list causes a 400 error. This fix detects the conflict in _map_function() and drops search tools, keeping function declarations. Non-search tools like code_execution and computerUse are preserved. Fixes #23337
…h_tool_conflict method
18b7291 to
dec630b
Compare
…13_2026_p1 litellm oss staging 04/13/2026
Background
The Gemini batchEmbedContents response handler hardcoded
index=0for every embedding in the response. Any consumer relying on the OpenAI-formatindexfield to match embeddings back to inputs would silently get wrong associations.Changes
Use
enumerateinprocess_responseso each embedding gets its positional index instead of 0.Test Plan
Added unit test asserting sequential indices and correct vector ordering for a 3-element batch response.
Relevant issues
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Screenshots / Proof of Fix
Type
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
Changes