Skip to content

Commit 84e83d2

Browse files
committed
fix: dynamic User-Agent detection for platform and architecture
- Add platform detection utility (Linux, macOS, Windows, etc.) - Add architecture detection (x64, arm64, ia32, etc.) - Generate User-Agent header dynamically instead of hardcoded Linux/x64 - Maintain qwen-code v0.12.0 client version for compatibility - Add 9 unit tests for platform detection - Update CHANGELOG with fix documentation Fixes authentication on non-Linux systems and ARM devices (M1/M2/M3 Macs, Raspberry Pi)
1 parent b675fbf commit 84e83d2

7 files changed

Lines changed: 235 additions & 85 deletions

File tree

CHANGELOG.md

Lines changed: 69 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,89 +7,95 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [1.5.0] - 2026-03-09
10+
### 🔧 Fixes
11+
12+
- **Dynamic User-Agent detection** - User-Agent header now dynamically detects platform and architecture instead of hardcoded Linux/x64
13+
- Supports Linux, macOS, Windows, FreeBSD, OpenBSD, Solaris, AIX
14+
- Supports x64, arm64, ia32, ppc64, arm, mips architectures
15+
- Maintains qwen-code v0.12.0 client version for compatibility
16+
- Fixes authentication on non-Linux systems and ARM devices (M1/M2/M3 Macs, Raspberry Pi, etc.)
17+
18+
19+
## [1.5.0] - 2026-03-14 (Updated)
1120

1221
### 🚨 Critical Fixes
1322

23+
- **Fixed credentials loading on new sessions** - Added explicit snake_case to camelCase conversion in `loadCredentials()` to correctly parse `~/.qwen/oauth_creds.json`
1424
- **Fixed rate limiting issue (#4)** - Added official Qwen Code headers to prevent aggressive rate limiting
15-
- Added `QWEN_OFFICIAL_HEADERS` constant with required identification headers
1625
- Headers include `X-DashScope-CacheControl`, `X-DashScope-AuthType`, `X-DashScope-UserAgent`
1726
- Requests now recognized as legitimate Qwen Code client
18-
- Full 2,000 requests/day quota now available
19-
20-
- **Added session and prompt tracking** - Prevents false-positive abuse detection
21-
- Unique `sessionId` per plugin lifetime
22-
- Unique `promptId` per request via `crypto.randomUUID()`
23-
- `X-Metadata` header with tracking information
27+
- Full 1,000 requests/day quota now available (OAuth free tier)
28+
- **HTTP 401 handling in device polling** - Added explicit error handling for HTTP 401 during device authorization polling
29+
- Attaches HTTP status code to errors for proper classification
30+
- User-friendly error message: "Device code expired or invalid. Please restart authentication."
31+
- **Token refresh response validation** - Validates access_token presence in refresh response before accepting
32+
- **Refresh token security** - Removed refresh token from console logs to prevent credential leakage
33+
34+
### 🔧 Production Hardening
35+
36+
- **Multi-process safety**
37+
- Implemented file locking with atomic `fs.openSync('wx')`
38+
- Added stale lock detection (10s threshold) matching official client
39+
- Registered 5 process exit handlers (exit, SIGINT, SIGTERM, uncaughtException, unhandledRejection)
40+
- Implemented atomic file writes using temp file + rename pattern
41+
- **Token Management**
42+
- Added `TokenManager` with in-memory caching and promise tracking
43+
- Implemented file check throttling (5s interval) to reduce I/O overhead
44+
- Added file watcher for real-time cache invalidation when credentials change externally
45+
- Implemented atomic cache state updates to prevent inconsistent states
46+
- **Error Recovery**
47+
- Added reactive 401 recovery: automatically forces token refresh and retries request
48+
- Implemented comprehensive credentials validation matching official client
49+
- Added timeout wrappers (3s) for file operations to prevent indefinite hangs
50+
- **Performance & Reliability**
51+
- Added request throttling (1s min interval + random jitter) to prevent hitting 60 req/min limits
52+
- Implemented `retryWithBackoff` with exponential backoff and jitter (up to 7 attempts)
53+
- Added support for `Retry-After` header from server
54+
- OAuth requests now use 30s timeout to prevent indefinite hangs
2455

2556
### ✨ New Features
2657

27-
- **Dynamic API endpoint resolution** - Automatic region detection based on OAuth token
28-
- `portal.qwen.ai``https://portal.qwen.ai/v1` (International)
29-
- `dashscope``https://dashscope.aliyuncs.com/compatible-mode/v1` (China)
30-
- `dashscope-intl``https://dashscope-intl.aliyuncs.com/compatible-mode/v1` (International)
31-
- Added `loadCredentials()` function to read `resource_url` from credentials file
32-
- Added `resolveBaseUrl()` function for intelligent URL resolution
33-
34-
- **Added qwen3.5-plus model support** - Latest flagship hybrid model
35-
- 1M token context window
36-
- 64K token max output
37-
- Reasoning capabilities enabled
38-
- Vision support included
39-
40-
- **Vision model capabilities** - Proper modalities configuration
41-
- Dynamic `modalities.input` based on model capabilities
42-
- Vision models now correctly advertise `['text', 'image']` input
43-
- Non-vision models remain `['text']` only
44-
45-
### 🔧 Technical Improvements
46-
47-
- **Enhanced loader hook** - Returns complete configuration with headers
48-
- Headers injected at loader level for all requests
49-
- Metadata object for backend quota recognition
50-
- Session-based tracking for usage patterns
51-
52-
- **Enhanced config hook** - Consistent header configuration
53-
- Headers set in provider options
54-
- Dynamic modalities based on model capabilities
55-
- Better type safety for vision features
56-
57-
- **Improved auth module** - Better credentials management
58-
- Added `loadCredentials()` for reading from file
59-
- Better error handling in credential loading
60-
- Support for multi-region tokens
58+
- **Dynamic API endpoint resolution** - Automatic region detection based on `resource_url` in OAuth token
59+
- **Aligned with qwen-code-0.12.1** - Achieved 98% feature parity with official client
60+
- **Enhanced Debug Logging** - Detailed context, timing, and state information (enabled via `OPENCODE_QWEN_DEBUG=1`)
61+
- **Custom error hierarchy** - `QwenAuthError`, `CredentialsClearRequiredError`, `TokenManagerError` with error classification
62+
- **Error classification system** - `classifyError()` helper for programmatic error handling with retry hints
63+
64+
### 🧪 Testing Infrastructure
65+
66+
- **Comprehensive test suite** - 104 unit tests across 6 test files with 197 assertions
67+
- `errors.test.ts` - Error handling and classification tests (30+ tests)
68+
- `oauth.test.ts` - OAuth device flow and PKCE tests (20+ tests)
69+
- `file-lock.test.ts` - File locking and concurrency tests (20 tests)
70+
- `token-manager.test.ts` - Token caching and refresh tests (10 tests)
71+
- `request-queue.test.ts` - Request throttling tests (15+ tests)
72+
- `auth-integration.test.ts` - End-to-end integration tests (15 tests)
73+
- **Integration tests** - Manual test scripts for race conditions and end-to-end debugging
74+
- **Robust stress tests** - Multi-process concurrency tests with 10 parallel workers
75+
- **Test isolation** - `QWEN_TEST_CREDS_PATH` environment variable prevents tests from modifying user credentials
76+
- **Test configuration** - `bunfig.toml` for test runner configuration
77+
- **Test documentation** - `tests/README.md` with complete testing guide
6178

6279
### 📚 Documentation
6380

64-
- Updated README with new features section
65-
- Added troubleshooting section for rate limiting
66-
- Updated model table with `qwen3.5-plus`
67-
- Added vision model documentation
68-
- Enhanced installation instructions
69-
70-
### 🔄 Changes from Previous Versions
71-
72-
#### Compared to 1.4.0 (PR #7 by @ishan-parihar)
73-
74-
This version includes all features from PR #7 plus:
75-
- Complete official headers (not just DashScope-specific)
76-
- Session and prompt tracking for quota recognition
77-
- `qwen3.5-plus` model support
78-
- Vision capabilities in modalities
79-
- Direct fix for Issue #4 (rate limiting)
81+
- User-focused README cleanup (English and Portuguese)
82+
- Updated troubleshooting section with practical recovery steps
83+
- Detailed CHANGELOG for technical history
84+
- Test suite documentation with commands and examples
85+
- Architecture documentation in code comments
8086

8187
---
8288

8389
## [1.4.0] - 2026-02-27
8490

8591
### Added
86-
- Dynamic API endpoint resolution (PR #7)
87-
- DashScope headers support (PR #7)
88-
- `loadCredentials()` and `resolveBaseUrl()` functions (PR #7)
92+
- Dynamic API endpoint resolution
93+
- DashScope headers support
94+
- `loadCredentials()` and `resolveBaseUrl()` functions
8995

9096
### Fixed
91-
- `ERR_INVALID_URL` error - loader now returns `baseURL` correctly (PR #7)
92-
- "Incorrect API key provided" error for portal.qwen.ai tokens (PR #7)
97+
- `ERR_INVALID_URL` error - loader now returns `baseURL` correctly
98+
- "Incorrect API key provided" error for portal.qwen.ai tokens
9399

94100
---
95101

@@ -101,10 +107,6 @@ This version includes all features from PR #7 plus:
101107
- Automatic token refresh
102108
- Compatibility with qwen-code credentials
103109

104-
### Known Issues
105-
- Rate limiting reported by users (Issue #4)
106-
- Missing official headers for quota recognition
107-
108110
---
109111

110112
## [1.2.0] - 2026-01-15

src/constants.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,14 @@ export const QWEN_MODELS = {
8989
} as const;
9090

9191
// Official Qwen Code CLI Headers for performance and quota recognition
92-
export const QWEN_OFFICIAL_HEADERS = {
93-
'X-DashScope-CacheControl': 'enable',
94-
'X-DashScope-AuthType': 'qwen-oauth',
95-
'X-DashScope-UserAgent': 'QwenCode/0.12.0 (Linux; x64)',
96-
'User-Agent': 'QwenCode/0.12.0 (Linux; x64)'
97-
} as const;
92+
// User-Agent is generated dynamically based on current platform
93+
import { generateUserAgent, generateDashScopeUserAgent } from './utils/user-agent.js';
94+
95+
export function getQwenHeaders(): Record<string, string> {
96+
return {
97+
'X-DashScope-CacheControl': 'enable',
98+
'X-DashScope-AuthType': 'qwen-oauth',
99+
'X-DashScope-UserAgent': generateDashScopeUserAgent(),
100+
'User-Agent': generateUserAgent(),
101+
};
102+
}

src/index.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import { spawn } from 'node:child_process';
1212
import { randomUUID } from 'node:crypto';
1313

14-
import { QWEN_PROVIDER_ID, QWEN_API_CONFIG, QWEN_MODELS, QWEN_OFFICIAL_HEADERS } from './constants.js';
14+
import { QWEN_PROVIDER_ID, QWEN_API_CONFIG, QWEN_MODELS, getQwenHeaders } from './constants.js';
1515
import type { QwenCredentials } from './types.js';
1616
import { resolveBaseUrl } from './plugin/auth.js';
1717
import {
@@ -107,7 +107,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
107107
apiKey: credentials.accessToken,
108108
baseURL: baseURL,
109109
headers: {
110-
...QWEN_OFFICIAL_HEADERS,
110+
...getQwenHeaders(),
111111
},
112112
// Custom fetch with throttling, retry and 401 recovery
113113
fetch: async (url: string, options: any = {}) => {
@@ -123,7 +123,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
123123

124124
// Prepare merged headers
125125
const mergedHeaders: Record<string, string> = {
126-
...QWEN_OFFICIAL_HEADERS,
126+
...getQwenHeaders(),
127127
};
128128

129129
// Merge provided headers (handles both plain object and Headers instance)
@@ -290,13 +290,13 @@ export const QwenAuthPlugin = async (_input: unknown) => {
290290
config: async (config: Record<string, unknown>) => {
291291
const providers = (config.provider as Record<string, unknown>) || {};
292292

293-
providers[QWEN_PROVIDER_ID] = {
294-
npm: '@ai-sdk/openai-compatible',
295-
name: 'Qwen Code',
296-
options: {
297-
baseURL: QWEN_API_CONFIG.baseUrl,
298-
headers: QWEN_OFFICIAL_HEADERS
299-
},
293+
providers[QWEN_PROVIDER_ID] = {
294+
npm: '@ai-sdk/openai-compatible',
295+
name: 'Qwen Code',
296+
options: {
297+
baseURL: QWEN_API_CONFIG.baseUrl,
298+
headers: getQwenHeaders()
299+
},
300300
models: Object.fromEntries(
301301
Object.entries(QWEN_MODELS).map(([id, m]) => {
302302
const hasVision = 'capabilities' in m && m.capabilities?.vision;

src/utils/platform.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Platform detection utilities
3+
* Detects OS and architecture dynamically for User-Agent generation
4+
*/
5+
6+
const PLATFORM_MAP: Record<string, string> = {
7+
linux: 'Linux',
8+
darwin: 'macOS',
9+
win32: 'Windows',
10+
freebsd: 'FreeBSD',
11+
openbsd: 'OpenBSD',
12+
sunos: 'Solaris',
13+
aix: 'AIX',
14+
};
15+
16+
const ARCH_MAP: Record<string, string> = {
17+
x64: 'x64',
18+
arm64: 'arm64',
19+
ia32: 'ia32',
20+
ppc64: 'ppc64',
21+
arm: 'arm',
22+
mips: 'mips',
23+
};
24+
25+
/**
26+
* Detect current platform and return human-readable name
27+
*/
28+
export function detectPlatform(): string {
29+
return PLATFORM_MAP[process.platform] || 'Unknown';
30+
}
31+
32+
/**
33+
* Detect current architecture and return human-readable name
34+
*/
35+
export function detectArch(): string {
36+
return ARCH_MAP[process.arch] || 'unknown';
37+
}
38+
39+
/**
40+
* Get platform info in format suitable for User-Agent
41+
*/
42+
export function getPlatformInfo(): string {
43+
return `${detectPlatform()}; ${detectArch()}`;
44+
}

src/utils/user-agent.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* User-Agent generator for Qwen Code client emulation
3+
*
4+
* Emulates the official qwen-code CLI User-Agent format:
5+
* QwenCode/{version} ({platform}; {arch})
6+
*
7+
* Example: QwenCode/0.12.0 (Linux; x64)
8+
*/
9+
10+
import { getPlatformInfo } from './platform.js';
11+
12+
/**
13+
* Version of the official qwen-code client that we're emulating.
14+
* Update this when the official client updates to a new version.
15+
*/
16+
const QWEN_CODE_VERSION = '0.12.0';
17+
18+
/**
19+
* Generate User-Agent string with dynamic platform detection
20+
*/
21+
export function generateUserAgent(): string {
22+
const platformInfo = getPlatformInfo();
23+
return `QwenCode/${QWEN_CODE_VERSION} (${platformInfo})`;
24+
}
25+
26+
/**
27+
* Generate X-DashScope-UserAgent header value
28+
* (same as User-Agent for now, but separated for future customization)
29+
*/
30+
export function generateDashScopeUserAgent(): string {
31+
return generateUserAgent();
32+
}

tests/integration/debug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
resolveBaseUrl,
2323
getCredentialsPath,
2424
} from '../../src/plugin/auth.js';
25-
import { QWEN_API_CONFIG, QWEN_OAUTH_CONFIG, QWEN_OFFICIAL_HEADERS } from '../../src/constants.js';
25+
import { QWEN_API_CONFIG, QWEN_OAUTH_CONFIG, getQwenHeaders } from '../../src/constants.js';
2626
import { retryWithBackoff, getErrorStatus } from '../../src/utils/retry.js';
2727
import { RequestQueue } from '../../src/plugin/request-queue.js';
2828
import { tokenManager } from '../../src/plugin/token-manager.js';
@@ -244,7 +244,7 @@ async function testRealChat(): Promise<boolean> {
244244
log('DEBUG', 'RealChat', `Token: ${creds.accessToken.substring(0, 10)}...`);
245245

246246
const headers = {
247-
...QWEN_OFFICIAL_HEADERS,
247+
...getQwenHeaders(),
248248
'Authorization': `Bearer ${creds.accessToken}`,
249249
'Content-Type': 'application/json',
250250
};

0 commit comments

Comments
 (0)