diff --git a/run_agent.py b/run_agent.py index afee105e8..d8ca03da0 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5555,7 +5555,13 @@ class AIAgent: # are programming bugs, not transient failures. _RETRYABLE_STATUS_CODES = {413, 429, 529} is_local_validation_error = isinstance(api_error, (ValueError, TypeError)) - is_client_status_error = isinstance(status_code, int) and 400 <= status_code < 500 and status_code not in _RETRYABLE_STATUS_CODES + # Detect generic 400s from Anthropic OAuth (transient server-side failures). + # Real invalid_request_error responses include a descriptive message; + # transient ones contain only "Error" or are empty. (ref: issue #1608) + _err_body = getattr(api_error, "body", None) or {} + _err_message = (_err_body.get("error", {}).get("message", "") if isinstance(_err_body, dict) else "") + _is_generic_400 = (status_code == 400 and _err_message.strip().lower() in ("error", "")) + is_client_status_error = isinstance(status_code, int) and 400 <= status_code < 500 and status_code not in _RETRYABLE_STATUS_CODES and not _is_generic_400 is_client_error = (is_local_validation_error or is_client_status_error or any(phrase in error_msg for phrase in [ 'error code: 401', 'error code: 403', 'error code: 404', 'error code: 422', diff --git a/tests/test_anthropic_adapter.py b/tests/test_anthropic_adapter.py index e36f0b2c9..7203de7e0 100644 --- a/tests/test_anthropic_adapter.py +++ b/tests/test_anthropic_adapter.py @@ -144,9 +144,11 @@ class TestIsClaudeCodeTokenValid: class TestResolveAnthropicToken: - def test_prefers_oauth_token_over_api_key(self, monkeypatch): + def test_prefers_oauth_token_over_api_key(self, monkeypatch, tmp_path): monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-mykey") monkeypatch.setenv("ANTHROPIC_TOKEN", "sk-ant-oat01-mytoken") + monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False) + monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path) assert resolve_anthropic_token() == "sk-ant-oat01-mytoken" def test_reports_claude_json_primary_key_source(self, monkeypatch, tmp_path): @@ -174,9 +176,11 @@ class TestResolveAnthropicToken: monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path) assert resolve_anthropic_token() == "sk-ant-api03-mykey" - def test_falls_back_to_token(self, monkeypatch): + def test_falls_back_to_token(self, monkeypatch, tmp_path): monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) monkeypatch.setenv("ANTHROPIC_TOKEN", "sk-ant-oat01-mytoken") + monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False) + monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path) assert resolve_anthropic_token() == "sk-ant-oat01-mytoken" def test_returns_none_with_no_creds(self, monkeypatch, tmp_path):