fix(batches): skip discarded tokenization on allowlist-only batch input file reads - #38330
fix(batches): skip discarded tokenization on allowlist-only batch input file reads#38330mynkyu wants to merge 22 commits into
Conversation
components/shared/Alert.tsx was base-vega's own alert.tsx copied in by hand, carrying the same four exports and the same class strings, so npx shadcn add could never reach it and it would drift from every upstream fix silently. It also still wrapped each part in forwardRef, which React 19 no longer needs. Install the primitive into components/ui/ where the CLI can update it, and reduce the shared file to a wrapper that adds the four status variants (info, success, warning, error) the dashboard actually uses on top of upstream's default and destructive. Rendered output is unchanged: every variant produces byte-identical classes, role and data-variant, so all 45 call sites look the same.
…arch-1e4443 refactor(ui): install the shadcn alert primitive
…e registry
These four primitives still wrapped their body in React.forwardRef, which
the dashboard has not needed since it moved to React 19: a function
component receives ref as an ordinary prop and the existing {...props}
spread already hands it to the DOM node.
Re-pulling each from base-vega drops the wrapper and its displayName.
These four were picked because the ref plumbing is their only divergence
from current upstream, so the class strings, data-slot values and exports
are untouched and nothing renders differently. The other seven primitives
that still carry forwardRef have also drifted on their class strings, so
re-pulling them would ship a visual change alongside the cleanup and they
are left alone here.
Textarea is the one with real ref call sites, roughly seventeen of them
through react-hook-form's field.ref, and ref-forwarding.test.tsx did not
cover it. Add that case next to the Label, Separator and Skeleton ones
already there.
…f_from_cli_primitives refactor(ui): re-pull label, textarea, separator and skeleton from the registry
* feat(prometheus): configure deployment caller identity * test(prometheus): satisfy strict caller identity lint * fix(prometheus): align caller identity on latency metrics * fix(prometheus): validate caller identity mode before collectors register Fail config load on an invalid prometheus_deployment_and_latency_caller_identity value (including null) and on include_labels entries the selected mode removes from a target metric, instead of booting green with an empty /metrics. Validate the mode at the top of PrometheusLogger.__init__ so an invalid value raises before any collector lands in the process-global registry, keeping retries free of duplicated-timeseries errors. Label-validation errors now name the mode setting alongside the rejected label. --------- Co-authored-by: Mark Philipp <mphilipp622@gmail.com> Co-authored-by: Yucheng Zhu <yucheng@berri.ai>
…type The fallback mapped every unbranched provider error by status code on every route, which changed the exception class and HTTP status for those providers and failed four provider test suites in CI. The /v1/messages handler change alone covers the ticket, since the anthropic branch already maps its errors
…ception_type unchanged Bugbot Autofix pushed e2e16d7 to split 403 out of the shared 401/403 branch in _map_openai_like_exception. That premise was the BaseLLMException fallback, which d2e4e74 already removed, and remapping 403 for every openai-like provider is a separate contract change, so this merge resolves both files back to the base branch versions
Now that /v1/messages routes provider failures through exception_type, an Anthropic permission_error fell through the anthropic branch to the generic APIConnectionError and reached the client as a 500 where the raw exception used to answer 403. Map 403 to PermissionDeniedError so the status survives on every route.
…play_hardening test(e2e): let the Together replayed-reasoning case survive a single provider miss
…ption_type branch
…essages_error_mapping fix(otel): map /v1/messages provider errors before failure logging
…itellm_fix_branchless_provider_status_mapping # Conflicts: # tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py
… streamed managed ids
…provider_status_mapping fix(exceptions): map upstream status codes for providers with no exception_type branch
…ect_ownership fix(passthrough): record ownership of streamed responses under managed ids
…e reads
`_should_skip_batch_input_file_processing` returns on the model-allowlist check
before it consults `disable_batch_input_file_rate_limiting` or the provider skip
list, so a key with a restricted `models` list always takes the full path even
when the operator opted out of batch input-file rate limiting. The download is
genuinely required there -- the allowlist can only be enforced by inspecting
every row -- but `count_input_file_usage` also runs `_count_entry_tokens` on
each row, and the caller then discards the totals because nothing charges them.
That tokenization is the expensive half. On a 652-row file it added ~10s to
every `POST /v1/batches`, which was enough to hold a proxy-wide average-latency
monitor above its threshold for as long as a batch was running (interactive
calls on the same deployment were 0.5~0.7s, and the same key's batch polling
was 0.55s). It is synchronous CPU work inside the async pre-call hook.
This adds a models-only read: `_batch_input_file_models_only()` reports when the
JSONL is needed solely for the allowlist, and `count_input_file_usage()` then
collects each row's `body.model` without tokenizing it.
Deliberately conservative:
- purely additive; no existing signature or return shape changes
- allowlist enforcement is untouched -- the file is still downloaded and every
`body.model` still validated, so the opt-out cannot become an authz bypass
- returns False whenever anything still needs the totals, including
enqueued-token scopes, whose reservation is priced from them
- the caller returns before the counters, leaving `_batch_*_count` unset,
matching the existing full-skip path rather than recording zeroes
Tests: 4 added to tests/test_litellm/proxy/hooks/test_batch_file_validation.py
covering the skip, the preserved 403 on a disallowed model, the unchanged
no-opt-out path, and the enqueued-scope guard. Suite goes 79 -> 83 passing;
reverting only the source change fails the tokenization test
("Expected '_count_entry_tokens' to not have been called. Called 2 times.").
|
|
Greptile SummaryThis PR combines the batch models-only optimization with broader changes to managed pass-through IDs, Prometheus caller identity, exception mapping, streaming utilities, and dashboard primitives
Confidence Score: 4/5The PR appears safe to merge after addressing the non-blocking repository-convention issues in the Prometheus changes The batch optimization retains model authorization, and no concrete runtime regression remains; accepted feedback is limited to mutable label transformation and unnecessary source comments Files Needing Attention: litellm/types/integrations/prometheus.py, litellm/integrations/prometheus.py
|
| Filename | Overview |
|---|---|
| litellm/proxy/hooks/batch_rate_limiter.py | Adds a models-only JSONL read that preserves allowlist checks while skipping unused tokenization |
| litellm/proxy/pass_through_endpoints/managed_id_rewriter.py | Adds buffered SSE response-ID ownership and rewriting for managed Responses API objects |
| litellm/types/integrations/prometheus.py | Adds caller-identity label modes, with an in-place transformation that violates the repository's immutability convention |
| litellm/integrations/prometheus.py | Validates caller-identity configuration and propagates user email through deployment failure metrics, but adds prohibited explanatory comments |
| litellm/litellm_core_utils/exception_mapping_utils.py | Maps additional provider HTTP statuses to normalized LiteLLM exceptions |
| ui/litellm-dashboard/src/components/shared/Alert.tsx | Wraps the new alert primitive while retaining application-specific status variants |
Reviews (1): Last reviewed commit: "fix(batches): skip discarded tokenizatio..." | Re-trigger Greptile
| if caller_identity == "user_email": | ||
| resolved_labels[alias_index] = UserAPIKeyLabelNames.USER_EMAIL.value | ||
| elif caller_identity == "both": | ||
| resolved_labels.insert(alias_index + 1, UserAPIKeyLabelNames.USER_EMAIL.value) | ||
|
|
||
| return resolved_labels |
There was a problem hiding this comment.
This resolver mutates resolved_labels in place, violating the repository’s immutable-data convention and complicating the label transformation
| if caller_identity == "user_email": | |
| resolved_labels[alias_index] = UserAPIKeyLabelNames.USER_EMAIL.value | |
| elif caller_identity == "both": | |
| resolved_labels.insert(alias_index + 1, UserAPIKeyLabelNames.USER_EMAIL.value) | |
| return resolved_labels | |
| if caller_identity == "user_email": | |
| return [ | |
| *resolved_labels[:alias_index], | |
| UserAPIKeyLabelNames.USER_EMAIL.value, | |
| *resolved_labels[alias_index + 1 :], | |
| ] | |
| if caller_identity == "both": | |
| return [ | |
| *resolved_labels[: alias_index + 1], | |
| UserAPIKeyLabelNames.USER_EMAIL.value, | |
| *resolved_labels[alias_index + 1 :], | |
| ] | |
| return resolved_labels |
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| # Validate the caller-identity mode before any collector registers so an | ||
| # invalid value cannot leave partially-registered metrics behind in the | ||
| # process-global registry. |
There was a problem hiding this comment.
Redundant implementation comments
These comments narrate the calls immediately below and duplicate ordinary behavior, adding prose that must remain synchronized with future changes
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| from litellm.types.integrations.prometheus import ( | ||
| _sanitize_prometheus_label_name, | ||
| _sanitize_prometheus_label_value, | ||
| validate_prometheus_deployment_and_latency_caller_identity, |
| from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER | ||
| from litellm.proxy._types import PassThroughEndpointLoggingResultValues | ||
| from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing | ||
| from litellm.proxy.common_utils.sse_keepalive import split_complete_sse_frames |
| prometheus_exclude_metrics: Optional[List[str]] = None | ||
| prometheus_exclude_labels: Optional[List[str]] = None | ||
| prometheus_emit_stream_label: bool = False | ||
| prometheus_deployment_and_latency_caller_identity: Literal[ |
| whenever anything still needs the totals, including enqueued-token | ||
| scopes, whose reservation is priced from them. | ||
| """ | ||
| from litellm.proxy.proxy_server import general_settings |
TLDR
Problem this solves:
POST /v1/batchesspends ~10s per 652-row file tokenizing rows whose token totals are then thrown awaymodelslist, even withdisable_batch_input_file_rate_limiting: true_should_skip_batch_input_file_processingreturns on the allowlist check firstHow it solves it:
body.model, skip_count_entry_tokensbody.modelstill validatedThe problem
_should_skip_batch_input_file_processingconsults the operator opt-outs after the model-allowlist check:The download itself is genuinely required — the allowlist can only be enforced by inspecting every row. But
count_input_file_usagealso runs_count_entry_tokens(a real tokenizer) per row, andasync_pre_call_hookthen discards the totals because nothing charges them.That tokenization is the expensive half, and it is synchronous CPU work inside the async pre-call hook.
Production measurements
Observed on a proxy running v1.96.2, during a nightly backfill submitting 652-row batches to
vertex_ai/gemini-2.5-flash:POST /v1/batches(652 rows)This held a proxy-wide average-latency monitor above its threshold for as long as a batch was running. Reproduced with zero 429s, so it is not retry-driven — it is the cost of the read itself.
The fix
_batch_input_file_models_only()reports when the JSONL is needed solely for the allowlistcount_input_file_usage(models_only=...)then collectsbody.modelwithout tokenizingasync_pre_call_hookreturns before counter enforcement in that mode, leaving_batch_*_countunset — matching the existing full-skip path rather than recording zeroesDeliberately conservative — it returns
Falsewhenever anything still needs the totals:Tests
4 added to
tests/test_litellm/proxy/hooks/test_batch_file_validation.py; suite goes 79 → 83 passing...._skips_tokenization_for_restricted_key_count_entry_tokensis never called and no counters are charged..._still_rejects_unauthorized_modeltest_restricted_key_without_opt_out_still_counts_tokens..._disabled_when_enqueued_scopes_applyasync_pre_call_hookswallows unexpected exceptions and returnsdata, so the returned value alone proves nothing — each test also asserts on the calls that separate the intended path from error recovery.Control: reverting only the source change (tests kept) fails the first test with
confirming the test is load-bearing rather than vacuous.
Relevant issues
I could not find an existing issue for this. Note that #35408 and #35813 touch the same function (bounding the input-file read with a deadline) — this change is orthogonal (it reduces work within the read) but will likely need a trivial rebase against whichever lands first.
Linear ticket
n/a — external contribution.
Pre-Submission checklist
Screenshots / Proof of Fix
Run against
mainin a container, with the import origin printed so it is unambiguous which tree is under test:Control, same command against an otherwise identical tree with only the source change reverted: