Skip to content

fix(sdk): classify a BadRequestError carrying HTTP 401/403 as an auth error - #5869

Open
enyst wants to merge 2 commits into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-validate-bad-key-type
Open

enyst wants to merge 2 commits into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-validate-bad-key-type

Conversation

@enyst

@enyst enyst commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
This PR proposes a fix for a bug uncovered during the first full agentic run of verify-agent-server.


AGENT:

Summary

 looks_like_auth_error(exception)
   existing typed authentication / permission checks
+  APIStatusError with status_code in {401, 403} → true
   existing message-pattern fallback

DeepSeek invalid key → LiteLLM BadRequestError(status=401) → LLMAuthenticationError.

Why

Some provider authentication failures arrive wrapped as BadRequestError and must still be classified as authentication errors.

Issue Number

Fixes #5862

How to Test

Previously recorded verification:

Reproduction commands, results, and limits
  • Reproduced live on main: litellm.completion(model='deepseek/deepseek-flash', api_key='sk-qa-invalid', ...) raises BadRequestError with status_code 401, and map_provider_exception returned LLMBadRequestError.
  • uv run pytest tests/sdk/llm/test_exception_mapping.py: the new test fails on main and passes here.
  • uv run pytest tests/sdk/llm tests/agent_server/test_profiles_router.py tests/sdk/conversation/local/test_auth_error_friendly_message.py: 1382 passed.
  • Map proof (AGENT_SERVER_VERIFY_HOME private, real DeepSeek):
    • control-agent-server map run --file F20 --only F20.validate-bad-key-type --fresh --repeat 2 --checkout <main checkout> gave xfail twice on main (6e7a853).
    • The same command from this branch gave xpass twice.
    • After the marker was removed, map check reported no problems and map run --only F20.validate-bad-key-type --fresh gave pass.
  • uv run pre-commit run --files <changed files> passes.

Video/Screenshots

No UI change; reproduction evidence is above.

Type

  • Bug fix

Notes

The same classification also enables the existing managed-key refresh hook for these 401s. The regression test constructs the provider response without a network call; recorded live evidence is below. Related typed-status mapping: #3222.

Visual explanation by Codex using HumanLayer show-me.


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:3df6f1a-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-3df6f1a-python \
  ghcr.io/openhands/agent-server:3df6f1a-python

All tags pushed for this build

ghcr.io/openhands/agent-server:3df6f1a-golang-amd64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-golang-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-golang-amd64
ghcr.io/openhands/agent-server:3df6f1a-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:3df6f1a-golang-arm64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-golang-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-golang-arm64
ghcr.io/openhands/agent-server:3df6f1a-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:3df6f1a-java-amd64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-java-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-java-amd64
ghcr.io/openhands/agent-server:3df6f1a-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:3df6f1a-java-arm64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-java-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-java-arm64
ghcr.io/openhands/agent-server:3df6f1a-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:3df6f1a-python-amd64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-amd64
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-amd64
ghcr.io/openhands/agent-server:3df6f1a-python-arm64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-arm64
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-arm64
ghcr.io/openhands/agent-server:3df6f1a-python-minimal-amd64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-minimal-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-minimal-amd64
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:3df6f1a-python-minimal-arm64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-minimal-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-minimal-arm64
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:3df6f1a-python-slim-amd64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-slim-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-slim-amd64
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:3df6f1a-python-slim-arm64
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-slim-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-slim-arm64
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:3df6f1a-golang
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-golang
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-golang
ghcr.io/openhands/agent-server:3df6f1a-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:3df6f1a-java
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-java
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-java
ghcr.io/openhands/agent-server:3df6f1a-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:3df6f1a-python-minimal
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-minimal
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-minimal
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-minimal
ghcr.io/openhands/agent-server:3df6f1a-python-slim
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python-slim
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python-slim
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime-slim
ghcr.io/openhands/agent-server:3df6f1a-python
ghcr.io/openhands/agent-server:3df6f1a99deb5c216ea726cc0322ebf498a888ef-python
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-validate-bad-key-type-python
ghcr.io/openhands/agent-server:3df6f1a-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., 3df6f1a-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 3df6f1a-python-amd64) are also available if needed

… error

LiteLLM raises DeepSeek's invalid-key 401 ("Authentication Fails, Your api
key: ... is invalid") as BadRequestError, but keeps the provider's status
code on the exception. looks_like_auth_error only matched message text, so
map_provider_exception (and profile validate) reported LLMBadRequestError.
Trust the 401/403 status code too, and drop the known-bug marker from the
F20.validate-bad-key-type map bullet.

Fixes #5862

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg
@enyst enyst self-assigned this Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/llm/exceptions
   classifier.py63395%118, 147, 179
TOTAL484501305173% 

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@enyst
enyst marked this pull request as ready for review October 14, 2026 01:07
@enyst
enyst requested a review from all-hands-bot October 14, 2026 01:11

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Summary

The fix is correct, minimal, and directly addresses the linked issue (#5862). In openhands-sdk/openhands/sdk/llm/exceptions/classifier.py, looks_like_auth_error now returns True for an openai.APIStatusError (which LiteLLM's BadRequestError is) carrying status_code 401/403, matching the existing typed AuthenticationError/PermissionDeniedError branch and the status 401/status 403 text check. The feature-map bullet loses its bug marker and the Gotchas line is removed.

Verification performed

  • Root cause confirmed live. Against the real DeepSeek API on the pinned litellm==1.93.0, completion(model='deepseek/deepseek-flash', api_key=<invalid>) raises BadRequestError with status_code == 401 and the message "Authentication Fails, Your api key: … is invalid", which matches none of AUTH_PATTERNS or the status 401/status 403 text. On the base it mapped to LLMBadRequestError; on this head map_provider_exception returns LLMAuthenticationError. The regression test does not depend on the network.
  • Acceptance criteria met. A plain 400 BadRequestError without auth wording still returns LLMBadRequestError; a 401/403 APIStatusError is classified as auth whatever its message; a unit test covers it without a network call; the F20.validate-bad-key-type marker is removed.
  • Tests run in the workspace. tests/sdk/llm/test_exception_mapping.py, tests/sdk/llm/test_exception_classifier.py, test_auth_refresh_retry.py, test_budget_denial_http.py, test_llm_fallback.py all pass; the broader tests/sdk/llm suite is 1380 passed. The only failures are tests/sdk/llm/auth/test_credentials.py::test_get_credentials_dir_default / ::test_get_credentials_dir_xdg, which fail because HOME=/var/openhands/.openhands in this environment (the expected path doubles .openhands); they are unrelated to this diff.
  • CI on the exact head (3cf4f8e9…). All check runs report success (pre-commit, smoke-test, integration-test, build-binary on ubuntu/macos/windows, coverage, OpenAPI breakage checks). No unresolved review threads.
  • The openai import is consistent with existing usage elsewhere in the SDK (openhands/sdk/tool/tool.py, openhands/sdk/mcp/tool.py), so it introduces no new undeclared-dependency pattern.

Non-blocking residual risk (no action required)

map_provider_exception evaluates auth before content policy, so with the new 401/403 branch a ContentPolicyViolationError that carries a 403 response would now map to LLMAuthenticationError instead of LLMContentPolicyViolationError (I reproduced this with constructed state; e.g. ContentPolicyViolationError(..., response=<403>)). The normal inline content-policy case is a 400 and is unaffected. I found no evidence that a supported provider returns content-policy blocks with HTTP 403, so I am not treating this as a defect; flagging it only so the human maintainer is aware that the auth branch is now ordered ahead of content policy for 403 responses.

No material correctness, security, compatibility, or acceptance-criterion defect found on the current head.

✅ APPROVED

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed after this PR merges to main through a cleanup PR.

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.

[Bug]: Profile validate reports an invalid DeepSeek key (HTTP 401) as LLMBadRequestError instead of LLMAuthenticationError

3 participants