From 06a5594bb615471fd4c3fc125e72cdb619ff80ca Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:25:55 -0700 Subject: [PATCH] fix(claude_code_gateway): mint the bearer before consuming the device code so a signing failure never spends the login --- .../anthropic_endpoints/gateway_endpoints.py | 5 ++--- .../test_gateway_endpoints.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/anthropic_endpoints/gateway_endpoints.py b/litellm/proxy/anthropic_endpoints/gateway_endpoints.py index 259d5202db6..0446992ae43 100644 --- a/litellm/proxy/anthropic_endpoints/gateway_endpoints.py +++ b/litellm/proxy/anthropic_endpoints/gateway_endpoints.py @@ -315,13 +315,12 @@ async def _handle_device_code_grant(device_code: str | None) -> JSONResponse: if isinstance(login, _OAuthError): return _oauth_error_response(login) + access_token: Final = _mint_access_token(login) if not await _claim_device_code(login_id, cli_sso_session_cache): return _oauth_error_response(_OAuthError(status_code=400, error="expired_token")) await cli_sso_session_cache.async_delete_cache(key=_get_cli_sso_flow_cache_key(login_id)) - body: Final = _AccessTokenBody( - access_token=_mint_access_token(login), expires_in=CLI_JWT_EXPIRATION_HOURS * _SECONDS_PER_HOUR - ) + body: Final = _AccessTokenBody(access_token=access_token, expires_in=CLI_JWT_EXPIRATION_HOURS * _SECONDS_PER_HOUR) return JSONResponse(content=body.model_dump()) diff --git a/tests/test_litellm/proxy/anthropic_endpoints/test_gateway_endpoints.py b/tests/test_litellm/proxy/anthropic_endpoints/test_gateway_endpoints.py index d442ac21307..7c3e8f56a21 100644 --- a/tests/test_litellm/proxy/anthropic_endpoints/test_gateway_endpoints.py +++ b/tests/test_litellm/proxy/anthropic_endpoints/test_gateway_endpoints.py @@ -320,6 +320,18 @@ def test_token_malformed_session_is_invalid_grant_and_does_not_consume_the_login mint.assert_not_called() +def test_token_mint_failure_leaves_the_login_unconsumed(): + with _gateway_env() as (client, cache): + device_code = _start_device_flow(client) + _complete_flow(cache, device_code) + with patch(_MINT, side_effect=RuntimeError("signing key unavailable")), pytest.raises(RuntimeError): + _request_token(client, device_code) + with patch(_MINT, return_value="sk-session"): + retry = _request_token(client, device_code) + assert retry.status_code == 200 + assert retry.json()["access_token"] == "sk-session" + + def test_token_unknown_team_grants_is_invalid_grant(): with _gateway_env() as (client, cache): device_code = _start_device_flow(client) @@ -349,11 +361,10 @@ def test_token_refuses_a_device_code_another_replica_already_claimed(): _set_cli_sso_flow(login_id=_SHARED_LOGIN_ID, cache=replica_a, flow=_completed_flow()) assert asyncio.run(gateway_endpoints._claim_device_code(_SHARED_LOGIN_ID, replica_a)) is True - with _gateway_env(cache=_replica(redis)) as (client, _), patch(_MINT) as mint: + with _gateway_env(cache=_replica(redis)) as (client, _), patch(_MINT, return_value="sk-session"): resp = _request_token(client, _SHARED_DEVICE_CODE) assert resp.status_code == 400 - assert resp.json()["error"] == "expired_token" - mint.assert_not_called() + assert resp.json() == {"error": "expired_token"} def test_token_unknown_device_code_is_expired_token():