Repository navigation
feat(batches): store each batch line's request and response in S3 and GCS - #41691
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…-j934-xhv5-fg8f Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…acks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…gation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai please re-review, the credentials leak is fixed and the unrelated bedrock diff is gone |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ettings store Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… events Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… merge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
|
@greptileai please re-review at the latest head |
Removing store_batch_line_items_in_callbacks from the DB general settings left the module flag at its previous value because the effect early-returned when the key was neither in the DB nor owned by YAML. The proxy owns the global, so an absent resolved value now writes False. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… transformer Anthropic result lines carry a message-shaped body, so ModelResponse(**body) produced empty choices and a usage key that blocked the batch-rate usage attach. Extract the existing AnthropicResponse -> ModelResponse conversion used for /v1/messages logging into anthropic_message_to_model_response and reuse it for anthropic line items. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@litereview code-review |
|
@litereview code-review |
|
bugbot run |
|
@litereview full |
…in parts The creator credential reaches line metadata only as a sha256 hash, so an OAuth2 token or the master key alias is never written to the bucket. A queued store keeps only whether the batch was billed and downloads the output file again at its turn, logging an error naming the batch if that fails. Lines are built and uploaded one part at a time, each closing once its raw request and result bytes reach BATCH_LINE_ITEMS_MAX_PART_BYTES (16 MiB by default), with the input file indexed by byte span instead of parsed up front.
|
@litereview code-review |
|
bugbot run |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@litereview full |
|
Automatic reviews are disabled for this repository. |
|
@litereview code-review |
|
bugbot run |
…ts line items are stored
|
@litereview code-review |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 315d705. Configure here.
|
@litereview full |
| ) | ||
|
|
||
|
|
||
| def _resolve_logger(callback: litellm.CALLBACK_TYPES) -> CustomLogger | None: |
There was a problem hiding this comment.
Low · Custom loggers named in batch_line_item_callbacks proxy config are never resolved, so the new hook cannot be reached from config
This is still unfixed at head 315d705be7 (previously upheld; the author deferred it to a follow-up).
CustomLogger.async_log_batch_line_items is added as a general extension point. _resolve_logger, however, accepts only a CustomLogger instance or the literal strings "s3_v2" and "gcs_bucket".
On the proxy, litellm_settings.batch_line_item_callbacks has no dedicated branch in proxy_server.py. It falls through to the generic setattr(litellm, key, value), so dotted paths are never turned into instances by get_instance_fn. The callbacks and audit_log_callbacks settings do get that step (proxy_server.py:6744-6762).
Failure scenario:
- An operator sets
batch_line_item_callbacks: ["custom_callbacks.proxy_handler_instance"]. - The
CustomLoggersubclass behind that path overridesasync_log_batch_line_items. - A managed batch completes.
_deliverlogscannot store batch line items, part N of batch ... was not storedand drops every part. The override is never called.
Nothing rejects the entry at startup, so the operator only finds out from runtime error logs after the batch's lines are already lost.
Fix: either add an elif key == "batch_line_item_callbacks" branch that resolves dotted entries with get_instance_fn, as audit_log_callbacks does, or reject unsupported names when the config loads.
Evidence
litellm/batches/batch_line_items.py:236-243: the match statement handles CustomLogger() and "s3_v2" | "gcs_bucket"; every other value hits case _: return None.
litellm/batches/batch_line_items.py _deliver: when logger is None, it logs an error and returns, so that part is never stored.
grep batch_line_item_callbacks litellm/proxy returns nothing: the proxy has no resolution step for this setting, so it reaches the generic setattr at proxy_server.py:~6853.
Latest summary by full · View this review · Reviewed 315d705be711
There was a problem hiding this comment.
Still a follow-up, same as the earlier thread. LIT-3395 asks only for S3 and GCS cold storage of batch lines, so this PR ships s3_v2 and gcs_bucket as the sinks, and the PR body lists module-path custom loggers under the Low caveats. Resolving dotted entries also means adding batch_line_item_callbacks to the DB config overlay's remote-module scrub next to audit_log_callbacks, so it belongs in its own PR with its own tests
There was a problem hiding this comment.
Finding still applies
At head 315d705be7, _resolve_logger still returns None for any callback other than a CustomLogger instance or the strings "s3_v2" / "gcs_bucket". The proxy has no branch that resolves dotted paths in batch_line_item_callbacks, so custom loggers configured by module path can't be reached and their parts are dropped at runtime. The author is deferring the fix to a follow-up for scope reasons. That is a scheduling decision, not evidence that the defect is gone.
Evidence
litellm/batches/batch_line_items.py:236-243: the match handles CustomLogger() and "s3_v2" | "gcs_bucket"; every other value hits case _: return None.
grep -rn batch_line_item_callbacks litellm/proxy returns no matches at head, so nothing resolves dotted entries with get_instance_fn.
The author's reply confirms the fix is deferred to a separate PR and does not claim it was fixed.
Reviewed 315d705be711 · View follow-up
TLDR
Problem this solves:
How it solves it:
batch_line_item_callbacks: ["s3_v2", "gcs_bucket"]User Flow
Before: an operator who keeps batch traffic in S3 for audits only ever sees the batch total
target_model_names, then POST https://litellm-domain/v1/batches with that file idAfter: the same operator finds every request and answer of the batch in their bucket
batch_line_item_callbacks: ["s3_v2"]underlitellm_settingsand restart the proxys3://<bucket>/<s3_path>/batch_line_items/<day>/<batch id>/part-00000.jsonlholds 100 lines, each with request, answer or error, tokens and costLinear ticket
Resolves LIT-3395
Docs: BerriAI/litellm-docs#2318
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or moreDelays 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
Before is the merge base f101347 and After is the head 315d705, the current tip
Setup: proxies at 2 workers with real OpenAI, Mistral, Bedrock and Vertex AI, the real S3 bucket
litellm-logs-test2and the real GCS bucketlitellm_bucket-16. A recorder in front of OpenAI and Mistral logs every request litellm sends them. The merge base and a head leg with the setting off run without thebatch_line_item_callbacksline. One flag-on proxy setsBATCH_LINE_ITEMS_MAX_PART_BYTES=20000to force several parts, and another accepts Google OAuth2 tokens (enable_oauth2_auth: true)The batches are all managed, and each has one line the provider fails on purpose: OpenAI (3 lines), OpenAI from a key with message logging off (3), Mistral (3), Bedrock (101), Vertex AI (3), Vertex AI with one line named
unknownand one with nocustom_id(3), and OpenAI created with a real Google OAuth2 token instead of a key (2)Before (f101347)
batch_line_items/in S3 is empty (KeyCount 0), and GCS has nothing for these batches eitherAfter (315d705)
Every line lands in S3 and GCS
batch_line_items/2026-10-15/<batch id>/, with Bedrock's 101 lines split into 4 parts by the 20000-byte limitThe OAuth2 batch's spend row is read off the Logs page, which rounds to six decimals
unknowngetrequest: nullinstead of another line's requestA key with message logging off
custom_id, status, usage, cost and creator, withrequestandresponsenullA batch created with an OAuth2 token
What litellm sent the providers
Audit cells
All cells ran at 315d705
tests/integration/observability/test_batch_line_items_cold_storage.py(real proxy at 2 workers, real Postgres and Redis, a scripted provider, local S3 and GCS): 13 passed in two head runs, and 12 failed at the merge base, where only the null-list cell passesBehavior changes
Removed / renamed
_parse_batch_output_lineis nowparse_batch_output_line, and two other helpers gain public aliases next to their old namesSilent regressions to watch
s3_v2listed, line-item writes share the request log's S3 failure metric, retry warning and upload limitMigration for existing customers
Regression tests
tests/integration/observability/test_batch_line_items_cold_storage.pypins the 13 cells abovetest_check_batch_cost.py,test_batch_line_items.py,test_s3_v2.pyandtest_gcs_bucket.pypin the order, background writing, parts, redaction, hashing and object namesType
🆕 New Feature
Caveats (if any)
Medium
target_model_names, get recordsLow
s3_v2andgcs_bucketwork from config, and custom loggers are a follow-uprequestis null for Anthropic batches and for Vertex AI lines reported asunknownbatch_processed, every worker writes the lines againFinal Attestation
Live A/B at 315d705 against the merge base f101347 with real providers, a real OAuth2 token and real buckets: with the setting off the head matches the merge base request for request, and with it on every line lands once with costs that add up to the spend row. ran /live-pr-risk and found no regressions/backward incompatible risks
REVIEWER MUST KNOW BEFORE APPROVING
Nothing changes unless
batch_line_item_callbacksis set. With it set, these changes are all opt-in and were agreed in the LIT-3395 design sessions:part-NNNNN.jsonlfiles of up to 16 MB, set byBATCH_LINE_ITEMS_MAX_PART_BYTESs3_v2line items reuse the request log's S3 loggerS3Loggerfailure metric, and their retries in its per-flush retry warnings3_adaptive_concurrencyon, a throttled line-item write lowers the shared upload limit, never belows3_max_concurrent_uploadsNot covered live: Azure (no Azure batch deployment here) and Anthropic (batch create returns 400, so nothing reaches the poller)
Link to Devin session: https://app.devin.ai/sessions/5a08a43ae7f140acb90639c83d8b450c
Open in Devin Desktop: https://app.devin.ai/desktop/session/5a08a43ae7f140acb90639c83d8b450c?variant=devin
Requested by: @yucheng-berri
Note
Medium Risk
Touches batch billing poller timing, reads batch files again for export, and handles creator credentials/logging—failures are best-effort but can drop line items without affecting batch cost.
Overview
Adds opt-in
batch_line_item_callbacksso finished managed batches emit per-line records (request, response/error, usage, cost, creator metadata) tos3_v2andgcs_bucket, not just a single batch spend row.After the batch-cost poller bills or retires a batch,
CheckBatchCostbuilds line items in boundedpart-NNNNN.jsonlchunks (viaBATCH_LINE_ITEMS_MAX_PART_BYTES) and dispatches them in the background—one batch at a time between poll cycles—so slow or failing bucket writes do not block billing. Request/response bodies are omitted when the creating key, team, or proxy has message logging off (or the creator cannot be resolved); credentials are stored only as SHA-256 hashes.New
litellm/batches/batch_line_items.pypairs JSONL lines across providers (including Vertex/Bedrock edge cases), prices each line, and exposesCustomLogger.async_log_batch_line_items. S3/GCS upload NDJSON underbatch_line_items/<day>/<batch id>/.Reviewed by Cursor Bugbot for commit 315d705. Bugbot is set up for automated code reviews on this repo. Configure here.