Skip to content

Commit eea3067

Browse files
committed
Auto-commit
1 parent 994afbc commit eea3067

3 files changed

Lines changed: 53 additions & 21 deletions

File tree

CLAUDE.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,26 @@ Environment variables in `.env.example`:
255255
- OAuth2: `CLAUDE_CODE_USE_OAUTH_CREDENTIALS`, `QWEN_CODE_USE_OAUTH_CREDENTIALS`
256256
- Cognee: `COGNEE_AUTH_EMAIL`, `COGNEE_AUTH_PASSWORD` (form-encoded OAuth2 auth)
257257

258-
### OAuth2 Authentication
258+
### OAuth2 Authentication (Limitations)
259259

260-
Claude and Qwen support OAuth2 via their CLI tools:
261-
- Claude: `claude auth login` creates `~/.claude/.credentials.json`
262-
- Qwen: creates `~/.qwen/oauth_creds.json`
260+
**IMPORTANT: OAuth tokens from CLI tools are product-restricted and cannot be used for general API calls.**
261+
262+
| Provider | Token Source | API Access |
263+
|----------|--------------|------------|
264+
| **Claude** | `~/.claude/.credentials.json` (from `claude auth login`) |**Restricted to Claude Code only** - cannot use for general API |
265+
| **Qwen** | `~/.qwen/oauth_creds.json` (from Qwen CLI login) |**For Qwen Portal only** - DashScope API requires separate API key |
266+
267+
**What works:**
268+
- HelixAgent successfully reads OAuth tokens from both credential files
269+
- Tokens are valid and non-expired
270+
271+
**What doesn't work:**
272+
- Using Claude OAuth tokens for general API requests returns: _"This credential is only authorized for use with Claude Code and cannot be used for other API requests."_
273+
- Using Qwen OAuth tokens for DashScope API returns: _"invalid_api_key"_ (tokens are for `portal.qwen.ai`)
274+
275+
**Solution:**
276+
- **Claude**: Get an API key from https://console.anthropic.com/
277+
- **Qwen**: Get a DashScope API key from https://dashscope.aliyuncs.com/
263278

264279
Key files: `internal/auth/oauth_credentials/`
265280

internal/auth/oauth_credentials/token_refresh.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ const (
2121
RefreshThreshold = 10 * time.Minute
2222

2323
// Claude OAuth endpoints
24-
ClaudeTokenEndpoint = "https://claude.ai/api/auth/oauth/token"
24+
ClaudeTokenEndpoint = "https://console.anthropic.com/v1/oauth/token"
2525

26-
// Qwen OAuth endpoints (Alibaba Cloud)
27-
QwenTokenEndpoint = "https://oauth.aliyun.com/v1/token"
26+
// Qwen OAuth endpoints (Qwen Code / Tongyi Lingma)
27+
QwenTokenEndpoint = "https://chat.qwen.ai/api/v1/oauth2/token"
28+
29+
// Qwen OAuth client ID (public client, used with PKCE)
30+
QwenOAuthClientID = "f0304373b74a44d2b584a3fb70ca9e56"
2831

2932
// HTTP client timeout for token refresh
3033
RefreshTimeout = 30 * time.Second
@@ -149,19 +152,15 @@ func (tr *TokenRefresher) RefreshQwenToken(refreshToken string, resourceURL stri
149152
return nil, fmt.Errorf("no refresh token available")
150153
}
151154

152-
// Determine token endpoint
155+
// Use the standard Qwen OAuth endpoint
156+
// Note: resourceURL is used by Qwen for the DashScope API, not for token refresh
153157
tokenEndpoint := QwenTokenEndpoint
154-
if resourceURL != "" {
155-
// Use resource-specific endpoint if available
156-
if u, err := url.Parse(resourceURL); err == nil {
157-
tokenEndpoint = fmt.Sprintf("%s://%s/oauth/token", u.Scheme, u.Host)
158-
}
159-
}
160158

161-
// Prepare refresh request
159+
// Prepare refresh request - Qwen requires client_id for token refresh
162160
data := url.Values{}
163161
data.Set("grant_type", "refresh_token")
164162
data.Set("refresh_token", refreshToken)
163+
data.Set("client_id", QwenOAuthClientID)
165164

166165
req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(data.Encode()))
167166
if err != nil {
@@ -182,6 +181,11 @@ func (tr *TokenRefresher) RefreshQwenToken(refreshToken string, resourceURL stri
182181
return nil, fmt.Errorf("failed to read refresh response: %w", err)
183182
}
184183

184+
// Handle HTTP 400 specifically - indicates refresh token is expired/invalid
185+
if resp.StatusCode == http.StatusBadRequest {
186+
return nil, fmt.Errorf("refresh token expired or invalid (HTTP 400): re-authentication required via Qwen Code CLI")
187+
}
188+
185189
if resp.StatusCode != http.StatusOK {
186190
return nil, fmt.Errorf("refresh failed with status %d: %s", resp.StatusCode, string(body))
187191
}

internal/llm/providers/claude/claude.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,9 @@ func (p *ClaudeProvider) getAuthHeader() (string, string, error) {
214214
if err != nil {
215215
return "", "", fmt.Errorf("failed to get OAuth token: %w", err)
216216
}
217-
// Claude OAuth tokens (sk-ant-oat01-*) from Claude Code CLI
218-
// NOTE: These tokens are designed for Claude Code infrastructure, not the public API.
219-
// The public api.anthropic.com does not accept OAuth tokens.
220-
// We use x-api-key header for compatibility, but this may fail for OAuth tokens.
221-
// When OAuth fails, the system falls back to other verified providers.
222-
return "x-api-key", token, nil
217+
// Claude OAuth tokens (sk-ant-oat01-*) use Bearer authentication
218+
// Required headers: Authorization: Bearer <token>, anthropic-beta: oauth-2025-04-20
219+
return "Authorization", "Bearer " + token, nil
223220
default:
224221
// Regular API keys use x-api-key header
225222
return "x-api-key", p.apiKey, nil
@@ -576,6 +573,12 @@ func (p *ClaudeProvider) makeAPICallWithAuthRetry(ctx context.Context, req Claud
576573
httpReq.Header.Set("anthropic-version", "2023-06-01")
577574
httpReq.Header.Set("User-Agent", "HelixAgent/1.0")
578575

576+
// Add OAuth-specific headers for Claude Code OAuth tokens
577+
if p.authType == AuthTypeOAuth {
578+
httpReq.Header.Set("anthropic-beta", "oauth-2025-04-20")
579+
httpReq.Header.Set("anthropic-product", "claude-code")
580+
}
581+
579582
// Make request
580583
resp, err := p.httpClient.Do(httpReq)
581584
if err != nil {
@@ -745,6 +748,12 @@ func (p *ClaudeProvider) HealthCheck() error {
745748
req.Header.Set(authHeaderName, authHeaderValue)
746749
req.Header.Set("anthropic-version", "2023-06-01")
747750

751+
// Add OAuth-specific headers for Claude Code OAuth tokens
752+
if p.authType == AuthTypeOAuth {
753+
req.Header.Set("anthropic-beta", "oauth-2025-04-20")
754+
req.Header.Set("anthropic-product", "claude-code")
755+
}
756+
748757
resp, err := p.httpClient.Do(req)
749758
if err != nil {
750759
return fmt.Errorf("health check request failed: %w", err)
@@ -753,6 +762,10 @@ func (p *ClaudeProvider) HealthCheck() error {
753762

754763
// Claude API returns 400 for GET requests to messages endpoint (expected)
755764
// We just check that the API is reachable and returns a response
765+
// For OAuth tokens, 401 means the token is invalid/expired
766+
if resp.StatusCode == http.StatusUnauthorized {
767+
return fmt.Errorf("health check failed: unauthorized (token may be expired or invalid)")
768+
}
756769
if resp.StatusCode >= 500 {
757770
return fmt.Errorf("health check failed with server error: %d", resp.StatusCode)
758771
}

0 commit comments

Comments
 (0)