{"record":{"id":"a5e4b7d38ed38d44","repo":"BerriAI/litellm","slug":"invalid-token-response-data","errorCode":null,"errorMessage":"Invalid token response: {data}","messagePattern":"Invalid token response: (.+?)","errorType":"exception","errorClass":"GigaChatAuthError","httpStatus":500,"severity":"error","filePath":"litellm/llms/gigachat/authenticator.py","lineNumber":229,"sourceCode":"            message=f\"GigaChat authentication failed: {e.response.text}\",\n        )\n    except httpx.RequestError as e:\n        raise GigaChatAuthError(\n            status_code=500,\n            message=f\"GigaChat authentication request failed: {e}\",\n        )\n\n\ndef _parse_token_response(response: httpx.Response) -> tuple[str, int]:\n    \"\"\"Parse OAuth token response.\"\"\"\n    data: Final = response.json()\n\n    # GigaChat returns either 'tok'/'exp' or 'access_token'/'expires_at'\n    access_token: Final = data.get(\"tok\") or data.get(\"access_token\")\n    expires_at = data.get(\"exp\") or data.get(\"expires_at\")\n\n    if not access_token:\n        raise GigaChatAuthError(\n            status_code=500,\n            message=f\"Invalid token response: {data}\",\n        )\n\n    # expires_at is in milliseconds\n    if isinstance(expires_at, str):\n        expires_at = int(expires_at)\n\n    verbose_logger.debug(\"GigaChat access token obtained successfully\")\n    return access_token, expires_at\n","sourceCodeStart":211,"sourceCodeEnd":240,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/gigachat/authenticator.py#L211-L240","documentation":"Raised inside _parse_token_response when the GigaChat OAuth endpoint returned HTTP 200 but the JSON body contains neither a 'tok' nor an 'access_token' field. litellm accepts both the native Sberbank shape ({tok, exp}) and an OAuth-style shape ({access_token, expires_at}); if both lookups are falsy it throws GigaChatAuthError(500) with the full parsed body in the message. This indicates an auth-endpoint contract change or an unexpected (possibly proxied/cached) 200 response.","triggerScenarios":"The POST to the auth URL succeeds with 2xx, but the body is e.g. an HTML login page served by a captive portal/proxy that httpx .json() happens to parse (or a JSON error envelope like {\"status\": 401, \"message\": ...} returned with 200), or GigaChat changed its token field names in an API revision. Also triggered when a custom GIGACHAT_API_BASE points at a gateway whose token response format differs from Sberbank's.","commonSituations":"Self-hosted API gateways or mock servers in front of GigaChat that return a different token schema; GigaChat API version drift between when the key was issued and now; a WAF rewriting the auth response; a custom api_base copied from a different integration tutorial.","solutions":["Inspect the data dict printed in the error message — it shows exactly which fields the endpoint returned instead of tok/access_token.","If using a custom GIGACHAT_API_BASE, verify the endpoint returns the Sberbank OAuth shape ({\"tok\": \"...\", \"exp\": <ms>}) or {\"access_token\": ..., \"expires_at\": ...}.","Test the raw exchange directly: curl -X POST <auth_url> -H \"Authorization: Basic <base64 key>\" -d 'scope=GIGACHAT_API_PERS' and inspect the JSON.","If the upstream format genuinely changed, pin/patch litellm's _parse_token_response or open an issue upstream with the response shape."],"exampleFix":"# before: custom gateway returns {\"accessToken\": ...} -> litellm raises 'Invalid token response'\nos.environ[\"GIGACHAT_API_BASE\"] = \"https://my-gateway.example.com\"\n\n# after: point at a gateway that passes through the native OAuth shape, or proxy-rewrite the field\n# (gateway config: map \"accessToken\" -> \"tok\", \"expiresInMs\" -> \"exp\")\nos.environ[\"GIGACHAT_API_BASE\"] = \"https://my-gateway.example.com\"  # now returns {\"tok\": ..., \"exp\": ...}","handlingStrategy":"validation","validationCode":"import base64, os, httpx\n\nkey = os.environ[\"GIGACHAT_API_KEY\"]\nscope = os.getenv(\"GIGACHAT_API_SCOPE\", \"GIGACHAT_API_PERS\")\nauth_url = os.getenv(\"GIGACHAT_API_BASE\", \"https://ngw.devices.sberbank.ru:9443\") + \"/api/v1/oauth\"\n\nresp = httpx.post(auth_url,\n    headers={\"Authorization\": f\"Basic {key}\", \"Content-Type\": \"application/x-www-form-urlencoded\"},\n    data={\"scope\": scope}, timeout=30, verify=False)\ndata = resp.json()\nassert data.get(\"tok\") or data.get(\"access_token\"), f\"Unexpected token response shape: {list(data)}\"","typeGuard":null,"tryCatchPattern":"from litellm.exceptions import AuthenticationError\n\ntry:\n    litellm.completion(model=\"gigachat/GigaChat-Pro\", messages=msgs)\nexcept AuthenticationError as e:\n    if \"Invalid token response\" in str(e):\n        # body is embedded; use it to diagnose gateway/contract drift\n        raise RuntimeError(f\"GigaChat auth endpoint returned unexpected JSON; inspect body in message: {e}\") from e\n    raise","preventionTips":["If proxying GigaChat auth, preserve the tok/exp field names exactly.","Pin litellm versions in production so contract changes surface in upgrades, not randomly.","Add a startup canary that performs one token exchange and asserts tok/access_token is present.","Keep the raw response body from the error message in incident reports — it names the divergence."],"tags":["gigachat","authentication","oauth","response-parsing","api-contract"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}