Note on root cause: The LiteLLM-side behavior described below is verified by reading the code on main and v1.86.0. The assumption that Claude Desktop's Code 3P tab sends Authorization: Bearer sk-ant-oat-... alongside x-api-key: sk-<litellm-key> is a strong hypothesis but has not been confirmed by capturing the actual request headers. It fits the symptoms (CLI and Cowork tab work with the same key; the error reports a token-not-found rather than a malformed key) but a maintainer or reporter should validate by capturing the headers Claude Code 3P actually sends before designing the fix.
Check for existing issues
Closest existing items, but none cover this specific bug:
What happened?
Users authenticating from the Claude Code 3P (Third-Party API) tab in Claude Desktop receive an authentication failure even when their LiteLLM virtual key is valid:
Authentication failed
Sign in again to continue.
Failed to authenticate. API Error: 401 Authentication Error, Invalid proxy server token passed.
Unable to find token in cache or LiteLLM_VerificationTokenTable
The same LiteLLM virtual key works correctly in:
- Claude Code CLI (
claude command)
- Claude Cowork 3P tab in Claude Desktop
It only fails in the Claude Code 3P tab in Claude Desktop.
Likely root cause (LiteLLM side — verified; client side — inferred)
Verified from the LiteLLM source: in litellm/proxy/auth/user_api_key_auth.py → get_api_key(), the precedence order is:
x-litellm-api-key
Authorization ← wins if present
API-Key (Azure)
x-api-key (Anthropic)
If Authorization carries any sk-...-shaped value, _get_bearer_token strips Bearer , the value gets hashed and looked up in LiteLLM_VerificationTokenTable, and a miss produces exactly this 401 at litellm/proxy/auth/user_api_key_auth.py:1314. The Anthropic x-api-key header is never inspected when Authorization is non-empty. There is no special-case in the auth layer for Anthropic OAuth tokens.
Inferred (not yet confirmed) about Claude Desktop: the most plausible explanation for the symptoms is that the Claude Code 3P tab sends both:
Authorization: Bearer sk-ant-oat-... — the Desktop session's logged-in Anthropic OAuth token (an upstream credential, not a LiteLLM key)
x-api-key: sk-... — the user's LiteLLM virtual key entered in the 3P provider settings
This fits the error mode (token-not-found rather than malformed) and explains why the CLI and Cowork tab work with the same key. A header capture from the failing 3P tab would confirm.
Why the CLI and Cowork tab work (assuming the hypothesis)
- The CLI sends only
x-api-key with the LiteLLM key — no Authorization header — so the Anthropic-header branch is correctly used.
- The Cowork tab apparently does not inject the Desktop OAuth token alongside the user's 3P key.
Notes
The codebase already recognizes that sk-ant-oat-* in an Authorization: Bearer header is a passthrough credential, not a proxy-auth credential — see is_anthropic_oauth_key() in litellm/llms/anthropic/common_utils.py:26 and the special-casing in litellm/proxy/litellm_pre_call_utils.py:589 and :2763. The proxy auth layer simply doesn't apply that same understanding when choosing which header to use as the LiteLLM key.
PRs #24454 and #24539 fix the closely-related outbound-header collision, but the inbound auth-precedence side of the bug remains.
Steps to Reproduce
You can reproduce the LiteLLM-side failure mode without Claude Desktop by simulating the headers the hypothesis says Claude Code 3P sends:
- Run a LiteLLM proxy with any valid virtual key (
sk-...).
- Send a request that combines an Anthropic OAuth token in
Authorization with the LiteLLM key in x-api-key:
curl -X POST http://<litellm-proxy>/v1/messages \
-H "Authorization: Bearer sk-ant-oat-01-EXAMPLE-OAUTH-TOKEN" \
-H "x-api-key: sk-<your-valid-litellm-virtual-key>" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 64,
"messages": [{"role": "user", "content": "ping"}]
}'
- Observe a 401 with the "Unable to find token in cache or
LiteLLM_VerificationTokenTable" error, even though x-api-key is a valid LiteLLM virtual key.
- Remove the
Authorization header — the same request with only x-api-key succeeds.
In Claude Desktop:
- Sign into Claude Desktop with an Anthropic account (this gives the Desktop client an OAuth session token).
- Open the Claude Code tab.
- Configure a Third-Party (3P) provider pointing at a LiteLLM proxy with a valid virtual key.
- Attempt to use Claude Code → "Authentication failed. Sign in again to continue." with the 401 above in the proxy logs.
The Claude Cowork tab and Claude Code CLI configured against the same proxy and key continue to work, isolating the issue to how Claude Code 3P composes its headers.
Relevant log output
litellm.proxy.proxy_server.user_api_key_auth(): Exception occurred -
ProxyException: Authentication Error, Invalid proxy server token passed.
Received API Key = sk-a***-oat..., Key Hash (Token) =<sha256(sk-ant-oat-...)>.
Unable to find token in cache or `LiteLLM_VerificationTokenTable`
HTTP 401 Unauthorized
What part of LiteLLM is this about?
Proxy
What LiteLLM version are you on?
v1.86.0 (also confirmed unfixed on main as of 2026-05-28 and on litellm_internal_staging / litellm_oss_staging_28_05_26)
Suggested fix (for maintainers)
Assuming the hypothesis holds: in litellm/proxy/auth/user_api_key_auth.py → get_api_key(), when the Authorization header carries an Anthropic OAuth token (sk-ant-oat-*), skip it for proxy auth and fall through to anthropic_api_key_header (x-api-key). The OAuth token is already understood elsewhere in the codebase as a pass-through credential for the upstream provider.
Conceptually:
elif isinstance(api_key, str) and len(api_key) > 0:
stripped = _get_bearer_token(api_key=api_key)
if is_anthropic_oauth_key(stripped) and isinstance(anthropic_api_key_header, str):
# Authorization carries an upstream Anthropic OAuth token, not the
# LiteLLM virtual key — let it pass through and use x-api-key for
# proxy auth instead.
passed_in_key = anthropic_api_key_header
api_key = anthropic_api_key_header
else:
passed_in_key = api_key
api_key = stripped
Test cases worth covering in tests/test_litellm/proxy/auth/test_user_api_key_auth.py:
Authorization only (no x-api-key) — existing behavior preserved.
x-api-key only — existing behavior preserved.
- Both, with
Authorization carrying an sk-ant-oat-* OAuth token — proxy auth uses x-api-key (new behavior).
- Both, with
Authorization carrying a regular LiteLLM key — proxy auth uses Authorization (existing behavior preserved).
Check for existing issues
Closest existing items, but none cover this specific bug:
user_api_key_auth), which fails before any upstream call is made.LiteLLM_VerificationTokenTable"), but unrelated cause (key cache regression in v1.82.3).What happened?
Users authenticating from the Claude Code 3P (Third-Party API) tab in Claude Desktop receive an authentication failure even when their LiteLLM virtual key is valid:
The same LiteLLM virtual key works correctly in:
claudecommand)It only fails in the Claude Code 3P tab in Claude Desktop.
Likely root cause (LiteLLM side — verified; client side — inferred)
Verified from the LiteLLM source: in
litellm/proxy/auth/user_api_key_auth.py→get_api_key(), the precedence order is:x-litellm-api-keyAuthorization← wins if presentAPI-Key(Azure)x-api-key(Anthropic)If
Authorizationcarries anysk-...-shaped value,_get_bearer_tokenstripsBearer, the value gets hashed and looked up inLiteLLM_VerificationTokenTable, and a miss produces exactly this 401 atlitellm/proxy/auth/user_api_key_auth.py:1314. The Anthropicx-api-keyheader is never inspected whenAuthorizationis non-empty. There is no special-case in the auth layer for Anthropic OAuth tokens.Inferred (not yet confirmed) about Claude Desktop: the most plausible explanation for the symptoms is that the Claude Code 3P tab sends both:
Authorization: Bearer sk-ant-oat-...— the Desktop session's logged-in Anthropic OAuth token (an upstream credential, not a LiteLLM key)x-api-key: sk-...— the user's LiteLLM virtual key entered in the 3P provider settingsThis fits the error mode (token-not-found rather than malformed) and explains why the CLI and Cowork tab work with the same key. A header capture from the failing 3P tab would confirm.
Why the CLI and Cowork tab work (assuming the hypothesis)
x-api-keywith the LiteLLM key — noAuthorizationheader — so the Anthropic-header branch is correctly used.Notes
The codebase already recognizes that
sk-ant-oat-*in anAuthorization: Bearerheader is a passthrough credential, not a proxy-auth credential — seeis_anthropic_oauth_key()inlitellm/llms/anthropic/common_utils.py:26and the special-casing inlitellm/proxy/litellm_pre_call_utils.py:589and:2763. The proxy auth layer simply doesn't apply that same understanding when choosing which header to use as the LiteLLM key.PRs #24454 and #24539 fix the closely-related outbound-header collision, but the inbound auth-precedence side of the bug remains.
Steps to Reproduce
You can reproduce the LiteLLM-side failure mode without Claude Desktop by simulating the headers the hypothesis says Claude Code 3P sends:
sk-...).Authorizationwith the LiteLLM key inx-api-key:LiteLLM_VerificationTokenTable" error, even thoughx-api-keyis a valid LiteLLM virtual key.Authorizationheader — the same request with onlyx-api-keysucceeds.In Claude Desktop:
The Claude Cowork tab and Claude Code CLI configured against the same proxy and key continue to work, isolating the issue to how Claude Code 3P composes its headers.
Relevant log output
What part of LiteLLM is this about?
Proxy
What LiteLLM version are you on?
v1.86.0 (also confirmed unfixed on
mainas of 2026-05-28 and onlitellm_internal_staging/litellm_oss_staging_28_05_26)Suggested fix (for maintainers)
Assuming the hypothesis holds: in
litellm/proxy/auth/user_api_key_auth.py→get_api_key(), when theAuthorizationheader carries an Anthropic OAuth token (sk-ant-oat-*), skip it for proxy auth and fall through toanthropic_api_key_header(x-api-key). The OAuth token is already understood elsewhere in the codebase as a pass-through credential for the upstream provider.Conceptually:
Test cases worth covering in
tests/test_litellm/proxy/auth/test_user_api_key_auth.py:Authorizationonly (nox-api-key) — existing behavior preserved.x-api-keyonly — existing behavior preserved.Authorizationcarrying ansk-ant-oat-*OAuth token — proxy auth usesx-api-key(new behavior).Authorizationcarrying a regular LiteLLM key — proxy auth usesAuthorization(existing behavior preserved).