Skip to content

fix(proxy): use find_unique directly to avoid 401 on nonexistent key#24064

Open
Jah-yee wants to merge 5 commits intoBerriAI:mainfrom
Jah-yee:fix/key-update-404
Open

fix(proxy): use find_unique directly to avoid 401 on nonexistent key#24064
Jah-yee wants to merge 5 commits intoBerriAI:mainfrom
Jah-yee:fix/key-update-404

Conversation

@Jah-yee
Copy link
Copy Markdown
Contributor

@Jah-yee Jah-yee commented Mar 19, 2026

Summary

Failure Path (Before Fix)

/key/update returns 401 for a nonexistent body key even when the Authorization header contains a valid master key. This happens because prisma_client.get_data() treats the token as an auth credential and raises a 401 when it doesn't exist in the DB.

Fix

Replace prisma_client.get_data() with direct Prisma find_unique() in both _get_and_validate_existing_key() and update_key_fn(), matching the pattern used in the /key/block and /key/unblock fix (PR #23977). Also fixes an incorrect "Team not found" error message in update_key_fn.

Testing

curl -s -X POST "http://localhost:4000/key/update" \
  -H "Authorization: Bearer $MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"sk-does-not-exist"}'
# Before: 401 "invalid user key"
# After:  404 "Key not found"

Fixes #24063

Jah-yee and others added 5 commits March 18, 2026 22:01
- LangSmith reads cost exclusively from outputs['usage_metadata']['total_cost']
- Previously, response_cost was computed but never exposed to LangSmith
- Now populates usage_metadata with token counts and total_cost from payload

Fixes BerriAI#24001
- Fix reasoning_tokens lookup: use completion_tokens_details (Chat
  Completions API) before output_tokens_details (Responses API)
- Add support for prompt_tokens_details (cached_tokens,
  cache_creation_tokens) in OTEL spans

This affects all OTEL-based callbacks: Langfuse, Arize, Phoenix, Weave.

Fixes: BerriAI#23990
- Add usage_metadata dict to outputs so LangSmith can display costs
- Fixes issue where Cost column always shows '-' despite response_cost being computed
- Maps prompt_tokens, completion_tokens, total_tokens, and response_cost to LangSmith format

Fixes BerriAI#24001
- Replace prisma_client.get_data() with direct find_unique() in
  _get_and_validate_existing_key() and update_key_fn()
- Fix incorrect error message from 'Team not found' to 'Key not found'

Fixes BerriAI#24063
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Error Error Mar 19, 2026 0:09am

Request Review

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 19, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
0 out of 2 committers have signed the CLA.

❌ penggaolai
❌ Jah-yee
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Mar 19, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing Jah-yee:fix/key-update-404 (3ebd7ba) with main (488b93c)

Open in CodSpeed

data.user_id = user_api_key_dict.user_id
verbose_proxy_logger.warning(
"key/generate: auto-assigning user_id=%s for non-admin caller",
user_api_key_dict.user_id,

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 19, 2026

Greptile Summary

This PR's stated goal — replacing prisma_client.get_data() with a direct find_unique() call in _get_and_validate_existing_key() and update_key_fn() to return a clean 404 (instead of an incorrect 401) when a nonexistent key is supplied to /key/update — is correct and well-motivated. The fix mirrors the approach used in the earlier /key/block and /key/unblock patch.

However, the PR bundles many additional, unrelated changes that each carry their own risk:

  • Accidentally committed file: langsmith.py is added at the repository root — a 561-line duplicate of litellm/integrations/langsmith.py from an earlier state. This file must be removed.
  • Backwards-incompatible behaviour changes without feature flags (violates project policy): non-admin callers creating keys without a user_id now have one silently auto-assigned; non-admin callers referencing a non-existent team_id now receive a 400 instead of succeeding. Both changes can break existing integrations and should be gated behind opt-in flags.
  • Redundant DB query: _check_key_admin_access() re-fetches a key row that was already retrieved in _validate_update_key_data(), causing an extra DB round-trip on every max_budget update.
  • is True identity check: disable_custom_api_keys is checked with is True rather than a truthy test, silently ignoring non-boolean truthy config values.
  • The remaining changes (arize/_utils.py reasoning-token fix, vertex_ai/common_utils.py deepcopy, langsmith.py cost metadata) are all sound.

The PR should be split or the backwards-incompatible additions should be protected behind flags before merging.

Confidence Score: 2/5

  • Not safe to merge as-is — contains an accidentally committed file at the repo root and multiple backwards-incompatible behaviour changes without feature flags.
  • The core 404 fix is correct and small, but the PR also introduces a stray root-level langsmith.py (which must be removed), silently breaking changes for non-admin key-generation workflows (auto user_id assignment and stricter team validation), and an extra DB query in the hot path. These unreviewed additions lower confidence significantly.
  • langsmith.py (root — accidental file), litellm/proxy/management_endpoints/key_management_endpoints.py (backwards-incompatible logic changes)

Important Files Changed

Filename Overview
langsmith.py Accidentally committed 561-line duplicate of litellm/integrations/langsmith.py at the repository root — should not exist and must be removed.
litellm/proxy/management_endpoints/key_management_endpoints.py Core fix (replacing get_data() with find_unique()) is correct, but the PR bundles many unrelated, backwards-incompatible behaviour changes (user_id auto-assignment, stricter team validation for non-admins, new admin-access checks for block/unblock) without feature flags, plus a redundant DB lookup in _check_key_admin_access().
litellm/integrations/langsmith.py Adds usage_metadata (including input_cost, output_cost, total_cost) to LangSmith run outputs so LangSmith's cost column is populated — straightforward and well-scoped.
litellm/integrations/arize/_utils.py Fixes reasoning token lookup to check both completion_tokens_details and output_tokens_details, and adds handling for prompt_tokens_details (cached/cache-creation tokens) — correct and non-breaking.
litellm/llms/vertex_ai/common_utils.py Adds a deepcopy of the schema before mutation in add_object_type() to prevent callers' original schema from being modified — correct defensive fix.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /key/update] --> B{Authorization header valid?}
    B -- No --> C[401 Unauthorized]
    B -- Yes --> D[find_unique on body key]
    D --> E{Key exists in DB?}
    E -- No --> F[404 Key not found]
    E -- Yes --> G[_validate_update_key_data]
    G --> H{max_budget changed?}
    H -- Yes --> I[_check_key_admin_access\nextra DB lookup]
    H -- No --> J[prepare_key_update_data]
    I --> J
    J --> K[Update DB row]
    K --> L[200 Updated key response]
Loading

Last reviewed commit: "fix(proxy): use find..."

Comment thread langsmith.py
@@ -0,0 +1,561 @@
#### What this does ####
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.

P0 Accidentally committed duplicate file at repository root

This file (langsmith.py) appears to have been accidentally committed to the repository root. The canonical Langsmith integration lives at litellm/integrations/langsmith.py.

This root-level copy is an older version — it notably lacks the usage_metadata fix that is being added to litellm/integrations/langsmith.py in this same PR. Keeping both will cause confusion about which file is authoritative and may shadow or conflict with the real integration in certain import contexts.

This file should be removed from the PR.

Comment on lines +1257 to +1262
if not _is_proxy_admin and data.user_id is None:
data.user_id = user_api_key_dict.user_id
verbose_proxy_logger.warning(
"key/generate: auto-assigning user_id=%s for non-admin caller",
user_api_key_dict.user_id,
)
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 Backwards-incompatible user_id auto-assignment for non-admin callers

Non-admin users calling /key/generate without specifying a user_id will now silently have the caller's user_id injected into the request. Previously, they could create unbound keys with no user_id. This is a behaviour change that is not gated behind a feature flag.

Per the project's backwards-compatibility policy, silent mutation of request fields for existing callers should be controlled by a user-configurable flag (e.g. litellm.auto_assign_user_id_on_key_generate). Without this, any automation or integration that relied on creating unowned keys as a non-admin user will start receiving keys tied to their own account, potentially breaking access-control assumptions.

Suggested change
if not _is_proxy_admin and data.user_id is None:
data.user_id = user_api_key_dict.user_id
verbose_proxy_logger.warning(
"key/generate: auto-assigning user_id=%s for non-admin caller",
user_api_key_dict.user_id,
)
if not _is_proxy_admin and data.user_id is None and litellm.auto_assign_user_id_on_key_generate:
data.user_id = user_api_key_dict.user_id
verbose_proxy_logger.warning(
"key/generate: auto-assigning user_id=%s for non-admin caller",
user_api_key_dict.user_id,
)

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +1279 to 1284
if not _is_proxy_admin:
raise HTTPException(
status_code=400,
detail=f"Team not found for team_id={data.team_id}. Non-admin users cannot create keys for non-existent teams.",
)

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 Backwards-incompatible enforcement for non-admin users referencing non-existent teams

Previously, when team_table is None (team not found) and litellm.key_generation_settings is None, key_generation_check() would return True, allowing any caller to assign an arbitrary team_id. The new code raises a 400 for non-admin callers.

This is a breaking change for existing users who create keys with a team_id that doesn't exist in the team table (a common pattern when bootstrapping or using team IDs as logical tags without full team table management). It should be controlled by a feature flag rather than being enforced unconditionally.

The same applies to the equivalent guard in generate_key_fn() at line 1279 — both changes together silently break existing non-admin workflows.

Rule Used: What: avoid backwards-incompatible changes without... (source)

return

ui_settings = await get_ui_settings_cached()
if ui_settings.get("disable_custom_api_keys", False) is True:
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.

P2 is True identity check may miss truthy non-boolean config values

Using is True is a strict identity check — it will evaluate to False if the setting is stored as a truthy non-boolean value such as 1, "true", or "yes" (common when config is sourced from environment variables or YAML). A regular truthy check is safer here:

Suggested change
if ui_settings.get("disable_custom_api_keys", False) is True:
if ui_settings.get("disable_custom_api_keys", False):

Comment on lines +4844 to +4848

# Look up the target key to find its team
target_key_row = await prisma_client.db.litellm_verificationtoken.find_unique(
where={"token": hashed_token}
)
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.

P2 Redundant DB lookup in _check_key_admin_access()

When called from _validate_update_key_data(), the key row has already been fetched from the database (as existing_key_row). The hashed_key passed in is just existing_key_row.token. Despite this, _check_key_admin_access() performs another find_unique call on the same table to retrieve the identical row.

Every /key/update call that changes max_budget therefore makes two separate database round-trips for the same record. Consider adding an optional key_row parameter to allow reuse of the already-fetched object and skip the redundant query.

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.

4 participants