Skip to content

fix(team callbacks): return callbacks registered via POST from GET /team/{team_id}/callback - #34991

Closed
devin-ai-integration[bot] wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_fix_team_callback_get
Closed

fix(team callbacks): return callbacks registered via POST from GET /team/{team_id}/callback#34991
devin-ai-integration[bot] wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_fix_team_callback_get

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • GET /team/{team_id}/callback always returned an empty list
  • Callbacks were visible in the UI but invisible to the API
  • disable_team_logging left the slot the proxy actually reads populated

How it solves it:

  • GET resolves both metadata.logging and metadata.callback_settings
  • Credential-bearing callback_vars come back redacted, not ciphertext
  • Disabling logging clears the logging entries too

Relevant issues

Linear ticket

Resolves LIT-4886

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 received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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

Live proxy on localhost:4000 (python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml), Postgres in docker, master key sk-1234

Setup, run identically before and after:

TEAM=$(curl -s -X POST http://localhost:4000/team/new -H 'Authorization: Bearer sk-1234' \
  -H 'Content-Type: application/json' -d '{"team_alias":"callback-repro"}' | jq -r .team_id)

curl -s -X POST "http://localhost:4000/team/$TEAM/callback" -H 'Authorization: Bearer sk-1234' \
  -H 'Content-Type: application/json' \
  -d '{"callback_name":"langsmith","callback_type":"success","callback_vars":{"langsmith_api_key":"lsv2-fake-key","langsmith_project":"repro-project"}}'

curl -s -X POST "http://localhost:4000/team/$TEAM/callback" -H 'Authorization: Bearer sk-1234' \
  -H 'Content-Type: application/json' \
  -d '{"callback_name":"langfuse","callback_type":"failure","callback_vars":{"langfuse_public_key":"pk-lf-1","langfuse_secret_key":"sk-lf-1"}}'

Before, at b930e2f (parent of this branch). Both POSTs return 200, the returned team row carries the callbacks under metadata.logging, and GET /team/info shows the same, yet the callback endpoint reports nothing:

$ curl -s -X GET "http://localhost:4000/team/$TEAM/callback" -H 'Authorization: Bearer sk-1234'
{
    "status": "success",
    "data": {
        "team_id": "35a04356-52a5-4182-a144-42369563add3",
        "success_callbacks": [],
        "failure_callbacks": [],
        "callback_vars": {}
    }
}

After, at 6f4dabe:

$ curl -s -X GET "http://localhost:4000/team/$TEAM/callback" -H 'Authorization: Bearer sk-1234'
{
    "status": "success",
    "data": {
        "team_id": "c13ba800-7f61-42c7-9211-9ff6992ad4c0",
        "success_callbacks": [
            "langsmith"
        ],
        "failure_callbacks": [
            "langfuse"
        ],
        "callback_vars": {
            "langsmith_api_key": "***REDACTED***",
            "langsmith_project": "repro-project",
            "langfuse_public_key": "***REDACTED***",
            "langfuse_secret_key": "***REDACTED***"
        }
    }
}

Disabling logging now empties the list; before the fix the logging slot survived the disable, so the team kept logging:

$ curl -s -X POST "http://localhost:4000/team/$TEAM/disable_logging" -H 'Authorization: Bearer sk-1234' -o /dev/null -w '%{http_code}\n'
200
$ curl -s -X GET "http://localhost:4000/team/$TEAM/callback" -H 'Authorization: Bearer sk-1234'
{
    "status": "success",
    "data": {
        "team_id": "c13ba800-7f61-42c7-9211-9ff6992ad4c0",
        "success_callbacks": [],
        "failure_callbacks": [],
        "callback_vars": {}
    }
}

Type

🐛 Bug Fix

Changes

Team callback config lives in two metadata slots. add_team_callbacks appends AddTeamCallback entries to metadata.logging, which is also what request-time resolution in litellm_pre_call_utils and the Admin UI read; metadata.callback_settings holds the older TeamCallbackMetadata shape used by config-driven setups and by disable_team_logging. get_team_callbacks read only the latter, so every callback registered through the API was invisible to the API

_resolve_team_callbacks folds both slots into one TeamCallbackMetadata, reusing convert_key_logging_metadata_to_callback so the GET reports exactly what the proxy will run, and skipping malformed logging entries the way request-time resolution already does:

base = TeamCallbackMetadata(**callback_settings)
resolved = reduce(
    lambda acc, callback: convert_key_logging_metadata_to_callback(data=callback, team_callback_settings_obj=acc),
    validated_logging_entries,
    base,
)
return resolved.model_copy(update={"callback_vars": _redact_sensitive_callback_vars(resolved.callback_vars)})

callback_vars are decrypted only so sensitive keys can be reported as ***REDACTED*** rather than as litellm_enc::... ciphertext; non-secret vars such as langsmith_project or gcs_bucket_name still come back usable

disable_team_logging also clears metadata.logging. It previously reset only callback_settings, so a team configured through the API kept logging after a disable, and with this GET fix would also keep reporting those callbacks

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/1838faf8459a4796abd976e2eb680285

…eam/{team_id}/callback

get_team_callbacks only read metadata.callback_settings, while add_team_callbacks writes to metadata.logging, so any callback registered through the API came back as an empty list even though the UI and request-time resolution both showed it. Resolve both slots, redact credential-bearing callback_vars, and have disable_team_logging clear the logging slot it left behind.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Comment on lines +109 to +114
)
resolved = reduce(
lambda acc, callback: convert_key_logging_metadata_to_callback(data=callback, team_callback_settings_obj=acc),
callbacks,
base,
)

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.

P1 GET diverges from runtime precedence

When a team has both callback_settings and logging, _resolve_team_callbacks returns their union while request-time resolution selects logging and skips callback_settings, causing GET to report callbacks and variables that requests never execute.

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!

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes team callback GET responses resolve both stored callback formats, redacts sensitive callback variables, and clears API-created callback entries when logging is disabled.

  • Adds a shared resolver for callback_settings and logging.
  • Extends disable behavior to empty metadata.logging.
  • Adds regression tests for API-created callbacks, mixed metadata formats, redaction, and disabling.

Confidence Score: 3/5

This PR should not merge until GET uses the same callback-source precedence as request-time execution and disabling clears legacy callback variables.

Mixed-format teams are reported as a union even though runtime selects one metadata slot, and disabling a legacy configuration leaves callback variables visible through the changed GET path.

Files Needing Attention: litellm/proxy/management_endpoints/team_callback_endpoints.py

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/team_callback_endpoints.py Adds callback resolution and disable cleanup, but GET no longer matches request-time source precedence and legacy variables survive disabling.
tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py Adds useful regression coverage but codifies union semantics and does not cover legacy callback variables after disabling.

Comments Outside Diff (1)

  1. litellm/proxy/management_endpoints/team_callback_endpoints.py, line 388-393 (link)

    P1 Disabled callbacks retain stale variables

    When a legacy team has values in callback_settings.callback_vars, this endpoint clears only the callback lists and persists those values, causing the changed GET path to continue returning stale callback configuration after logging is disabled.

Reviews (1): Last reviewed commit: "fix(team callbacks): return callbacks re..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.47619% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...xy/management_endpoints/team_callback_endpoints.py 90.47% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_team_callback_get (6f4dabe) with litellm_internal_staging (f4a68a7)

Open in CodSpeed

@yucheng-berri

Copy link
Copy Markdown
Contributor

Closing in favor of #35512, which resolves LIT-5093 with live-proxy evidence.

The one substantive difference worth recording: this PR's resolver merges metadata["callback_settings"] and metadata["logging"], while request-time resolution in _get_dynamic_logging_metadata treats them as mutually exclusive (a present logging slot wins outright and callback_settings is only the deprecated fallback). For a team carrying both shapes the merged view reports a destination as active that its requests never send to. #35512 mirrors the runtime precedence instead, and verifies it by resolving every team row on a live rig through both this endpoint and the real runtime resolver.

Thanks for the original diagnosis; the metadata-slot mismatch you identified was correct and is what #35512 fixes.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Agreed on the precedence point, and thanks for catching it. _get_dynamic_logging_metadata is an elif chain, so a present logging slot short-circuits callback_settings entirely; merging them made the GET report a destination that requests never reach. #35512 is the right shape

One thing to make sure doesn't get dropped: this PR also cleared the logging slot in disable_team_logging, which is what LIT-5101 tracks. Happy to pick that one up separately if it isn't already assigned

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.

2 participants