From bcceb2bc2ca7aacbef4c290a45962a44b0b38e1c Mon Sep 17 00:00:00 2001 From: wylab Date: Fri, 13 Feb 2026 12:57:21 +0100 Subject: [PATCH] feat(providers): add OAuth token detection and header utilities Co-Authored-By: Claude Opus 4.6 --- nanobot/providers/oauth_utils.py | 46 ++++++++++++++++++++++++++++++++ tests/test_oauth_utils.py | 28 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 nanobot/providers/oauth_utils.py create mode 100644 tests/test_oauth_utils.py diff --git a/nanobot/providers/oauth_utils.py b/nanobot/providers/oauth_utils.py new file mode 100644 index 0000000..f2e32e2 --- /dev/null +++ b/nanobot/providers/oauth_utils.py @@ -0,0 +1,46 @@ +"""OAuth utility functions for Anthropic subscription auth.""" + +from typing import Any + + +def is_oauth_token(token: str | None) -> bool: + """Check if token is an OAuth token (vs regular API key). + + OAuth tokens from Claude Max/Pro contain 'sk-ant-oat' prefix. + Regular API keys use 'sk-ant-api03' or similar. + """ + if not token: + return False + return "sk-ant-oat" in token + + +def get_auth_headers(token: str, is_oauth: bool = False) -> dict[str, str]: + """Get authentication headers for Anthropic API. + + OAuth tokens require Authorization: Bearer header. + Regular API keys use x-api-key header. + """ + headers: dict[str, str] = { + "anthropic-version": "2023-06-01", + "content-type": "application/json", + } + + if is_oauth: + headers["Authorization"] = f"Bearer {token}" + # Required headers to mimic Claude Code client + headers["anthropic-beta"] = "claude-code-20250219,oauth-2025-04-20" + headers["anthropic-dangerous-direct-browser-access"] = "true" + headers["user-agent"] = "claude-cli/2.1.2 (external, cli)" + headers["x-app"] = "cli" + else: + headers["x-api-key"] = token + + return headers + + +def get_claude_code_system_prefix() -> str: + """Get the required system prompt prefix for OAuth tokens. + + Anthropic requires this identity declaration for OAuth auth. + """ + return "You are Claude Code, Anthropic's official CLI for Claude." diff --git a/tests/test_oauth_utils.py b/tests/test_oauth_utils.py new file mode 100644 index 0000000..c84af4c --- /dev/null +++ b/tests/test_oauth_utils.py @@ -0,0 +1,28 @@ +"""Test OAuth utility functions.""" +import pytest +from nanobot.providers.oauth_utils import is_oauth_token, get_auth_headers + + +def test_is_oauth_token_detects_oat(): + """Should detect sk-ant-oat tokens as OAuth.""" + assert is_oauth_token("sk-ant-oat01-buSdhCH2XEkebW7ZQZTvGqH5EwAFh4u52LrdJhAP") is True + assert is_oauth_token("sk-ant-api03-regularkey") is False + assert is_oauth_token("") is False + assert is_oauth_token(None) is False + + +def test_get_auth_headers_oauth(): + """OAuth tokens should use Authorization: Bearer.""" + headers = get_auth_headers("sk-ant-oat01-xxx", is_oauth=True) + assert "Authorization" in headers + assert headers["Authorization"] == "Bearer sk-ant-oat01-xxx" + assert "x-api-key" not in headers + assert headers["anthropic-beta"] == "claude-code-20250219,oauth-2025-04-20" + + +def test_get_auth_headers_api_key(): + """Regular API keys should use x-api-key.""" + headers = get_auth_headers("sk-ant-api03-xxx", is_oauth=False) + assert "x-api-key" in headers + assert headers["x-api-key"] == "sk-ant-api03-xxx" + assert "Authorization" not in headers