Commit 252d45e4 authored by Your Name's avatar Your Name

fix: Use SDK auth_token parameter with OAuth2 token (not API key)

Your theory was correct! Claude Code uses the Anthropic SDK with the
authToken parameter (not apiKey) for OAuth2 authentication.

From vendors/claude/src/services/api/client.ts lines 300-315:
    const clientConfig = {
      apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
      authToken: isClaudeAISubscriber()
        ? getClaudeAIOAuthTokens()?.accessToken
        : undefined,
    }
    return new Anthropic(clientConfig)

Changes:
- providers.py: Use auth_token=access_token (not api_key) for SDK client
- claude_auth.py: Remove create_api_key() and get_api_key() methods
  (not needed - OAuth2 token is used directly with SDK auth_token)

The create_api_key endpoint is only for creating API keys for use in
other contexts (CI/CD, IDEs), not for the main CLI.
parent 9bb2c090
...@@ -581,97 +581,6 @@ class ClaudeAuth: ...@@ -581,97 +581,6 @@ class ClaudeAuth:
logger.error(f"Token exchange failed after {max_retries} attempts") logger.error(f"Token exchange failed after {max_retries} attempts")
return False return False
def create_api_key(self, access_token: str = None) -> Optional[str]:
"""
Exchange OAuth2 access token for an API key.
This matches the Claude Code flow:
1. Get OAuth2 access token
2. Call create_api_key endpoint to get an API key
3. Use the API key for API requests (not the OAuth2 token)
See: vendors/claude/src/services/oauth/client.ts:createAndStoreApiKey()
Endpoint: https://api.anthropic.com/api/oauth/claude_cli/create_api_key
Args:
access_token: OAuth2 access token (uses current token if not provided)
Returns:
API key string or None if failed
"""
if access_token is None:
if not self.tokens or 'access_token' not in self.tokens:
logger.warning("No access token available")
return None
access_token = self.tokens['access_token']
try:
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
# The endpoint that exchanges OAuth2 token for an API key
api_key_url = "https://api.anthropic.com/api/oauth/claude_cli/create_api_key"
response = self._make_request(
method="POST",
url=api_key_url,
headers=headers,
json_data=None,
timeout=30.0
)
if response.status_code == 200:
data = response.json()
api_key = data.get('raw_key')
if api_key:
# Store the API key in credentials
if self.tokens:
self.tokens['api_key'] = api_key
self._save_credentials(self.tokens)
logger.info("Successfully created and stored API key")
return api_key
else:
logger.warning(f"API key not found in response: {data}")
return None
else:
logger.error(f"API key creation failed: {response.status_code} - {response.text}")
return None
except Exception as e:
logger.error(f"API key creation error: {e}")
return None
def get_api_key(self) -> Optional[str]:
"""
Get a valid API key for API requests.
If we have a stored API key, return it.
If not, create one from the OAuth2 access token.
Returns:
API key string or None if failed
"""
# Check if we have a stored API key
if self.tokens and 'api_key' in self.tokens:
return self.tokens['api_key']
# No API key stored, create one from OAuth2 token
if self.tokens and 'access_token' in self.tokens:
logger.info("No stored API key, creating one from OAuth2 token...")
return self.create_api_key()
# No tokens at all, need to login
logger.info("No tokens available, starting login flow")
self.login()
# Try to create API key after login
if self.tokens and 'access_token' in self.tokens:
return self.create_api_key()
return None
def is_authenticated(self) -> bool: def is_authenticated(self) -> bool:
"""Check if we have valid credentials.""" """Check if we have valid credentials."""
return self.tokens is not None and 'access_token' in self.tokens return self.tokens is not None and 'access_token' in self.tokens
......
...@@ -2350,52 +2350,57 @@ class ClaudeProviderHandler(BaseProviderHandler): ...@@ -2350,52 +2350,57 @@ class ClaudeProviderHandler(BaseProviderHandler):
def _get_sdk_client(self): def _get_sdk_client(self):
""" """
Get or create an Anthropic SDK client configured with API key. Get or create an Anthropic SDK client configured with OAuth2 auth token.
The SDK handles proper message formatting, retries, and streaming. Claude Code uses the SDK with authToken parameter (not apiKey).
We use the API key obtained from the OAuth2 token exchange, See vendors/claude/src/services/api/client.ts lines 300-315:
matching the Claude Code flow (see createAndStoreApiKey in client.ts).
const clientConfig = {
apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
authToken: isClaudeAISubscriber()
? getClaudeAIOAuthTokens()?.accessToken
: undefined,
}
return new Anthropic(clientConfig)
""" """
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Get API key from OAuth2 token exchange # Get valid OAuth2 access token
# This matches the Claude Code flow: OAuth2 token → API key → API requests access_token = self.auth.get_valid_token()
api_key = self.auth.get_api_key()
if not api_key: if not access_token:
logger.error("ClaudeProviderHandler: Failed to get API key from OAuth2 token") logger.error("ClaudeProviderHandler: No OAuth2 access token available")
raise Exception("Failed to get API key. Please re-authenticate with /login") raise Exception("No OAuth2 access token. Please re-authenticate with /login")
# Create SDK client with API key # Create SDK client with OAuth2 auth token (not API key)
# This matches the Claude Code implementation exactly
self._sdk_client = Anthropic( self._sdk_client = Anthropic(
api_key=api_key, auth_token=access_token, # OAuth2 token, not API key
base_url="https://api.anthropic.com",
max_retries=3, # SDK handles automatic retries max_retries=3, # SDK handles automatic retries
timeout=httpx.Timeout(300.0, connect=30.0), timeout=httpx.Timeout(300.0, connect=30.0),
) )
logger.info("ClaudeProviderHandler: Created SDK client with API key") logger.info("ClaudeProviderHandler: Created SDK client with OAuth2 auth token")
return self._sdk_client return self._sdk_client
def _get_auth_headers(self, stream: bool = False): def _get_auth_headers(self, stream: bool = False):
""" """
Get HTTP headers with OAuth2 Bearer token. Get HTTP headers with OAuth2 Bearer token.
Matches CLIProxyAPI header structure for compatibility. Used for direct HTTP calls (not SDK).
Kept for backward compatibility with direct HTTP calls.
""" """
import logging import logging
logger = logging.getLogger(__name__)
# Get valid OAuth2 access token # Get valid OAuth2 access token
access_token = self.auth.get_valid_token() access_token = self.auth.get_valid_token()
# Build headers matching CLIProxyAPI/Claude Code implementation # Build headers matching Claude Code implementation
headers = { headers = {
'Authorization': f'Bearer {access_token}', 'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Anthropic-Version': '2023-06-01', 'Anthropic-Version': '2023-06-01',
'Anthropic-Beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05', 'Anthropic-Beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05',
'Anthropic-Dangerous-Direct-Browser-Access': 'true',
'X-App': 'cli', 'X-App': 'cli',
'X-Stainless-Retry-Count': '0', 'X-Stainless-Retry-Count': '0',
'X-Stainless-Runtime': 'node', 'X-Stainless-Runtime': 'node',
...@@ -2412,7 +2417,7 @@ class ClaudeProviderHandler(BaseProviderHandler): ...@@ -2412,7 +2417,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
headers['Accept'] = 'application/json' headers['Accept'] = 'application/json'
headers['Accept-Encoding'] = 'gzip, deflate, br, zstd' headers['Accept-Encoding'] = 'gzip, deflate, br, zstd'
logging.info("ClaudeProviderHandler: Created auth headers with OAuth2 Bearer token") logger.info("ClaudeProviderHandler: Created auth headers with OAuth2 Bearer token")
return headers return headers
def _sanitize_tool_call_id(self, tool_call_id: str) -> str: def _sanitize_tool_call_id(self, tool_call_id: str) -> str:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment