Skip to content

fix(logging): classify allm_passthrough_route as async to prevent duplicate success callbacks - #32265

Merged
yucheng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_lit4192_bedrock_passthrough_langsmith_dup
Jul 6, 2026
Merged

fix(logging): classify allm_passthrough_route as async to prevent duplicate success callbacks#32265
yucheng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_lit4192_bedrock_passthrough_langsmith_dup

Conversation

@yucheng-berri

@yucheng-berri yucheng-berri commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-4192

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Repro environment: postgres on 127.0.0.1:5432, litellm proxy on :4000, fake Bedrock on :5091, fake LangSmith capture on :5092, team-level LangSmith success callback registered via POST /team/{team_id}/callback, one plain sync callback in litellm_settings.success_callback (the required trigger; see LIT-4192 "Trigger condition"). The Bedrock request is a real boto3 converse against the proxy's /bedrock/... passthrough

Before (unfixed, on origin/litellm_internal_staging @ 29035c4a99):

=== RUN 1: Bedrock passthrough ===
A tiny fox found a star and shared its light.
total langsmith runs captured: 3
litellm_call_id=5a5dc7cb-5218-4e79-81b7-e996b2f2b86e run_count=3
  response_ids = ['chatcmpl-8fec6f35-...', 'chatcmpl-8fec6f35-...', 'chatcmpl-d398d129-...']
  call_types   = ['allm_passthrough_route', 'allm_passthrough_route', 'allm_passthrough_route']
  outcome      = ['success', 'success', 'success']

=== RUN 2: Bedrock passthrough (same key/team, thread race gives variance) ===
A tiny fox found a star and shared its light.
total langsmith runs captured: 3
litellm_call_id=9a3381e4-1851-40ca-99ea-29023d15c77d run_count=3
  response_ids = ['chatcmpl-d3a0bf84-...', 'chatcmpl-d3a0bf84-...', 'chatcmpl-9872d602-...']
  call_types   = ['allm_passthrough_route', 'allm_passthrough_route', 'allm_passthrough_route']
  outcome      = ['success', 'success', 'success']

=== CONTROL: /chat/completions (unaffected, exactly 1 run) ===
A tiny fox found a star and shared its light.
total langsmith runs captured: 1
litellm_call_id=2dde1f1f-0c29-4c54-b835-13ef4459d3a1 run_count=1
  response_ids = ['chatcmpl-61d22f26-...']
  call_types   = ['acompletion']
  outcome      = ['success']

Matches the fingerprint on the ticket: same litellm_call_id, different chatcmpl- response ids, count varying with the thread race, /chat/completions unaffected

After (this branch):

=== RUN 1: Bedrock passthrough (expect exactly 1) ===
A tiny fox found a star and shared its light.
total langsmith runs captured: 1
litellm_call_id=c24640c4-9878-41e4-addb-313f4a09bc4f run_count=1
  response_ids = ['chatcmpl-fa4d4b6a-6398-4896-b82f-5f2752057da9']
  call_types   = ['allm_passthrough_route']
  outcome      = ['success']

=== RUN 2: Bedrock passthrough (expect exactly 1) ===
A tiny fox found a star and shared its light.
total langsmith runs captured: 1
litellm_call_id=fdf34cc2-6a91-4469-998b-f8ed27ebc19b run_count=1

=== RUN 3-7: 5 back-to-back requests, each should be 1 ===
run 1: total langsmith runs captured: 1
run 2: total langsmith runs captured: 1
run 3: total langsmith runs captured: 1
run 4: total langsmith runs captured: 1
run 5: total langsmith runs captured: 1

=== CONTROL: /chat/completions (regression check, still 1) ===
total langsmith runs captured: 1
litellm_call_id=30b0117a-3c31-4b06-9587-091d616b9d66 run_count=1
  call_types   = ['acompletion']
  outcome      = ['success']

Exactly one LangSmith run per Bedrock passthrough request across 7 consecutive requests, /chat/completions unchanged

Type

Bug Fix

Changes

Logging._is_sync_litellm_request() (litellm/litellm_core_utils/litellm_logging.py) decides sync vs async by inspecting litellm_params for acompletion / aresponses / aembedding / aimage_generation / atranscription. The async passthrough entrypoint in litellm/passthrough/main.py sets kwargs["allm_passthrough_route"] = True, but that flag was never propagated into litellm_params, so _is_sync_litellm_request returned True for every async passthrough request. That trips the CustomLogger branch inside the sync success_handler (isinstance(callback, CustomLogger) and is_sync_request and call_type != CallTypes.pass_through.value), which calls LangsmithLogger.log_success_event() on top of the async worker's async_log_success_event(), producing duplicate LangSmith runs

Fix at the classifier level so the whole request path treats async passthrough consistently as async:

  • get_litellm_params now accepts allm_passthrough_route and writes it into the returned litellm_params dict, matching the pattern used for acompletion and aembedding
  • llm_passthrough_route forwards its allm_passthrough_route argument through the get_litellm_params(...) call so the flag reaches litellm_params
  • _is_sync_litellm_request recognizes allm_passthrough_route=True as async, using the same shape as the existing checks

Regression tests in tests/test_litellm/litellm_core_utils/test_litellm_logging.py:

  • test_is_sync_litellm_request grows an assertion that {"allm_passthrough_route": True} classifies as async
  • test_get_litellm_params_propagates_allm_passthrough_route locks in that get_litellm_params(allm_passthrough_route=True) produces params that _is_sync_litellm_request reads as async, so the flag survives the propagation step end to end
  • test_success_handler_skips_sync_callbacks_for_async_requests gains allm_passthrough_route as a parametrize case, so a future regression that reintroduces the sync-callback branch for async passthrough fails immediately

Reverted against litellm_internal_staging, all three of these tests fail; with the fix all pass, and the full tests/test_litellm/litellm_core_utils/test_litellm_logging.py suite (109 tests) is green

Regression audit

Full sweep for regressions and backwards-incompatible changes; every claim below is grounded in code evidence

Callers of _is_sync_litellm_request (four total)

Location Behavior change for passthrough Behavior change for anything else
litellm_logging.py:1577 (dispatch_success_handlers) passthrough now enters async path instead of sync shortcut none; other call types don't set the new flag
litellm_logging.py:1971 (success_handler) CustomLogger sync branch (line 2308) skipped for passthrough, stopping the duplicate none
litellm_logging.py:2759 (failure_handler) CustomLogger failure branch (line 2842) skipped for passthrough, preventing a symmetric duplicate on failures none
streaming_handler.py:1671 (CustomStreamWrapper.run_success_logging_and_cache_storage) no real impact; passthrough streaming uses _sync_streaming/_async_streaming in passthrough/main.py, not CustomStreamWrapper none

Writers of kwargs["allm_passthrough_route"] = True: exactly one site, passthrough/main.py:63. No other code, tests, or docs set this key, so no non-passthrough request can accidentally flip to async

Callers of get_litellm_params: 48 across litellm/ and tests/. All use **kwargs or explicit keyword arguments; no positional callers, so adding allm_passthrough_route=None as a keyword-only parameter is safe

Downstream consumers of the returned litellm_params dict: none read "allm_passthrough_route" as a key outside _is_sync_litellm_request. Not serialized to SpendLogsPayload, StandardLoggingPayload, DB rows, or callback payloads. The new None value on non-passthrough requests is invisible in every observable path

@client decorator and signature introspection: litellm/utils.py client/function_setup/wrapper_async do not use inspect.signature/__signature__. Dropping allm_passthrough_route: bool = False from llm_passthrough_route's signature is safe

Callers of llm_passthrough_route(...): no external caller passes allm_passthrough_route=True as a keyword. The only writer is the internal allm_passthrough_route which sets kwargs["allm_passthrough_route"] = True; the flag now flows through **kwargs naturally

Orthogonality with the old pass_through_endpoint guard: CallTypes.pass_through.value == "pass_through_endpoint" and CallTypes.allm_passthrough_route.value == "allm_passthrough_route" are distinct strings for distinct code paths; no redundancy, no contradiction

Empirical test-diff (baseline origin/litellm_internal_staging vs HEAD)

Suite: tests/test_litellm/litellm_core_utils/ + tests/test_litellm/passthrough/ + tests/test_litellm/proxy/pass_through_endpoints/ + tests/test_litellm/proxy/guardrails/test_deferred_guardrail_logging.py + tests/pass_through_unit_tests/test_unit_test_streaming.py

Regressions introduced by this PR: 0

Newly passing on this branch: 4 — exactly the LIT-4192 tests added or extended here (test_is_sync_litellm_request new assertion, test_get_litellm_params_propagates_allm_passthrough_route, test_success_handler_skips_sync_callbacks_for_async_requests[allm_passthrough_route], test_llm_passthrough_route_propagates_allm_passthrough_route_to_logging_obj); on origin/litellm_internal_staging they fail because the source fix is absent, which serves as the mutation check for this PR


Note

Medium Risk
Changes logging callback routing for passthrough requests; scoped fix with tests but affects observability integrations on a hot path.

Overview
Fixes duplicate success callbacks (e.g. LangSmith runs) on async LLM passthrough by treating allm_passthrough_route like other async entrypoints.

get_litellm_params now accepts and stores allm_passthrough_route in litellm_params. Logging._is_sync_litellm_request treats allm_passthrough_route=True as async so the sync success_handler does not also fire CustomLogger.log_success_event on top of the async path.

llm_passthrough_route no longer takes allm_passthrough_route as a dedicated parameter; it reads kwargs["allm_passthrough_route"] for async detection while the flag still flows through get_litellm_params(**kwargs) into the logging object.

Regression tests cover param propagation, sync/async classification, and skipping sync callbacks for allm_passthrough_route.

Reviewed by Cursor Bugbot for commit 335f99e. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


yucheng seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes duplicate success callbacks fired for async LLM passthrough requests by ensuring _is_sync_litellm_request correctly classifies them as async. The root cause was that allm_passthrough_route=True was set in kwargs by the async entrypoint but never reached litellm_params, so the sync-callback branch inside success_handler fired in addition to the async worker.

  • get_litellm_params now accepts allm_passthrough_route as a named parameter and writes it into the returned dict, mirroring the existing acompletion/aembedding pattern.
  • _is_sync_litellm_request gains a corresponding CallTypes.allm_passthrough_route.value check so any async passthrough request is classified as async end-to-end.
  • llm_passthrough_route drops the now-redundant explicit parameter and reads the flag directly from **kwargs, with get_litellm_params(**kwargs) propagating it naturally; three targeted test assertions lock in both the classifier logic and the propagation path.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to how the async passthrough flag is propagated and classified, with no side-effects on other call paths.

All three layers of the fix (propagation via get_litellm_params, classification in _is_sync_litellm_request, and the llm_passthrough_route kwarg read) are consistent with the existing patterns for acompletion/aembedding, and CallTypes.allm_passthrough_route is already defined in the enum. The regression tests directly cover the propagation path and the sync-callback skip, and the PR description includes before/after output demonstrating the fix.

No files require special attention.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/get_litellm_params.py Adds allm_passthrough_route as an explicit named parameter and propagates it into the returned litellm_params dict, following the same pattern as acompletion and aembedding. Change is minimal and correct.
litellm/litellm_core_utils/litellm_logging.py Adds allm_passthrough_route check to _is_sync_litellm_request using the existing CallTypes enum (value confirmed in litellm/types/utils.py), consistent with all other async-flag checks in the method.
litellm/passthrough/main.py Removes the now-redundant explicit allm_passthrough_route parameter and reads the flag from **kwargs instead; get_litellm_params(**kwargs) then captures and propagates it. The _is_async derivation and downstream update_environment_variables call are unaffected.
tests/test_litellm/litellm_core_utils/test_litellm_logging.py Three test assertions added/extended: test_is_sync_litellm_request (unit check), test_get_litellm_params_propagates_allm_passthrough_route (propagation check), and test_success_handler_skips_sync_callbacks_for_async_requests gains allm_passthrough_route parametrize case. All cover the regression path cleanly.

Reviews (3): Last reviewed commit: "test(passthrough): assert allm_passthrou..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes duplicate success callbacks (e.g. duplicate LangSmith runs) when using the async Bedrock/LLM passthrough route. The allm_passthrough_route flag, already set in kwargs by the async entrypoint, was never propagated into litellm_params, so _is_sync_litellm_request incorrectly returned True for every async passthrough request and triggered the sync CustomLogger branch on top of the async worker's callback.

  • get_litellm_params now accepts allm_passthrough_route as an explicit parameter and writes it into the returned litellm_params dict, exactly as is done for acompletion and aembedding.
  • _is_sync_litellm_request in litellm_logging.py adds CallTypes.allm_passthrough_route to its async-flag checks, and llm_passthrough_route explicitly forwards the flag when calling get_litellm_params.
  • Three focused regression tests are added (all mock-only): a new parametrize case on the skip-sync-callbacks test, a new assertion in test_is_sync_litellm_request, and a new end-to-end propagation test that guards the whole flag-to-classification path.

Confidence Score: 5/5

The change is minimal and scoped entirely to the async-vs-sync classifier and its propagation path; no auth, DB, or routing logic is touched.

Three files receive targeted, one-line-or-fewer additions that follow established patterns already in the codebase (acompletion, aembedding). The fourth file adds strictly additive, mock-only tests that strengthen regression coverage. The before/after proof in the PR description directly matches the code path being fixed.

No files require special attention.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/get_litellm_params.py Adds allm_passthrough_route as an explicit parameter and writes it into the returned litellm_params dict, matching the pattern used for acompletion and aembedding; no issues found.
litellm/litellm_core_utils/litellm_logging.py Extends _is_sync_litellm_request to treat allm_passthrough_route=True as async, preventing the sync-callback branch from firing on async passthrough requests; follows the exact same pattern as the existing aembedding/atranscription checks.
litellm/passthrough/main.py Forwards the allm_passthrough_route flag explicitly into the get_litellm_params(...) call so the flag survives into litellm_params; Python correctly de-dupes it since allm_passthrough_route is an explicit named param in both functions.
tests/test_litellm/litellm_core_utils/test_litellm_logging.py Adds three targeted regression guards: one parametrized case in the existing skip-sync-callbacks test, one new unit assertion in test_is_sync_litellm_request, and one new end-to-end propagation test; all are mock-only and strengthen (not weaken) coverage.

Reviews (1): Last reviewed commit: "fix(logging): classify allm_passthrough_..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…licate success callbacks

Async passthrough requests set kwargs["allm_passthrough_route"]=True but that
flag is never propagated into litellm_params, and _is_sync_litellm_request only
checks acompletion/aresponses/aembedding/aimage_generation/atranscription.
Every async passthrough is misclassified as sync, which trips the CustomLogger
sync branch in success_handler and fires log_success_event in addition to the
async worker's async_log_success_event, causing 2-3 duplicate LangSmith runs
per Bedrock passthrough request

Propagate allm_passthrough_route through get_litellm_params and teach the
classifier about it. /chat/completions and other non-passthrough paths are
untouched
@yucheng-berri
yucheng-berri force-pushed the litellm_lit4192_bedrock_passthrough_langsmith_dup branch from b02d694 to 3bf4daf Compare July 6, 2026 20:25
…to-end

Integration-level guard on top of the unit tests in test_litellm_logging.py:
verifies that when kwargs["allm_passthrough_route"]=True enters
llm_passthrough_route, the flag survives get_litellm_params(**kwargs), lands
in the logging object's litellm_params, and _is_sync_litellm_request reads
the request as async
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

Force-pushed once earlier to clean up the propagation path (dropped the redundant allm_passthrough_route param from llm_passthrough_route's signature; **kwargs carries the flag naturally now). Then added a real follow-up commit with an integration test in tests/test_litellm/passthrough/test_passthrough_main.py that locks in the end-to-end propagation of the flag from kwargs through get_litellm_params to litellm_params on the logging object. Mutation-checked against the source revert.

@greptileai please re-review the current HEAD

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 335f99e. Configure here.

@yucheng-berri
yucheng-berri merged commit 101f246 into litellm_internal_staging Jul 6, 2026
130 of 132 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_lit4192_bedrock_passthrough_langsmith_dup branch July 6, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants