Skip to content

Commit 25c789c

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 5fd7cf4 commit 25c789c

7 files changed

Lines changed: 181 additions & 24 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

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+
1019
## [1.5.0] - 2026-03-14 (Updated)
1120

1221
### 🚨 Critical Fixes

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 {
@@ -108,7 +108,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
108108
apiKey: credentials.accessToken,
109109
baseURL: baseURL,
110110
headers: {
111-
...QWEN_OFFICIAL_HEADERS,
111+
...getQwenHeaders(),
112112
},
113113
// Custom fetch with throttling, retry and 401 recovery
114114
fetch: async (url: string, options: any = {}) => {
@@ -124,7 +124,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
124124

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

130130
// Merge provided headers (handles both plain object and Headers instance)
@@ -266,13 +266,13 @@ export const QwenAuthPlugin = async (_input: unknown) => {
266266
config: async (config: Record<string, unknown>) => {
267267
const providers = (config.provider as Record<string, unknown>) || {};
268268

269-
providers[QWEN_PROVIDER_ID] = {
270-
npm: '@ai-sdk/openai-compatible',
271-
name: 'Qwen Code',
272-
options: {
273-
baseURL: QWEN_API_CONFIG.baseUrl,
274-
headers: QWEN_OFFICIAL_HEADERS
275-
},
269+
providers[QWEN_PROVIDER_ID] = {
270+
npm: '@ai-sdk/openai-compatible',
271+
name: 'Qwen Code',
272+
options: {
273+
baseURL: QWEN_API_CONFIG.baseUrl,
274+
headers: getQwenHeaders()
275+
},
276276
models: Object.fromEntries(
277277
Object.entries(QWEN_MODELS).map(([id, m]) => {
278278
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/debug.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ import {
1515
refreshAccessToken,
1616
isCredentialsExpired,
1717
SlowDownError,
18-
} from '../src/qwen/oauth.js';
18+
} from '../../src/qwen/oauth.js';
1919
import {
2020
loadCredentials,
2121
saveCredentials,
2222
resolveBaseUrl,
2323
getCredentialsPath,
24-
} from '../src/plugin/auth.js';
25-
import { QWEN_API_CONFIG, QWEN_OAUTH_CONFIG, QWEN_OFFICIAL_HEADERS } from '../src/constants.js';
26-
import { retryWithBackoff, getErrorStatus } from '../src/utils/retry.js';
27-
import { RequestQueue } from '../src/plugin/request-queue.js';
28-
import { tokenManager } from '../src/plugin/token-manager.js';
29-
import type { QwenCredentials } from '../src/types.js';
24+
} from '../../src/plugin/auth.js';
25+
import { QWEN_API_CONFIG, QWEN_OAUTH_CONFIG, getQwenHeaders } from '../../src/constants.js';
26+
import { retryWithBackoff, getErrorStatus } from '../../src/utils/retry.js';
27+
import { RequestQueue } from '../../src/plugin/request-queue.js';
28+
import { tokenManager } from '../../src/plugin/token-manager.js';
29+
import type { QwenCredentials } from '../../src/types.js';
3030

3131
// ============================================
3232
// Logging Utilities
@@ -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
};

tests/unit/platform.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, test, expect } from 'bun:test';
2+
import { detectPlatform, detectArch, getPlatformInfo } from '../../src/utils/platform.js';
3+
import { generateUserAgent } from '../../src/utils/user-agent.js';
4+
5+
describe('Platform Detection', () => {
6+
test('should detect current platform', () => {
7+
const platform = detectPlatform();
8+
expect(platform).toBeTruthy();
9+
expect(typeof platform).toBe('string');
10+
expect(platform.length).toBeGreaterThan(0);
11+
});
12+
13+
test('should detect current architecture', () => {
14+
const arch = detectArch();
15+
expect(arch).toBeTruthy();
16+
expect(typeof arch).toBe('string');
17+
expect(arch.length).toBeGreaterThan(0);
18+
});
19+
20+
test('should return valid platform info format', () => {
21+
const platformInfo = getPlatformInfo();
22+
expect(platformInfo).toContain('; ');
23+
const [platform, arch] = platformInfo.split('; ');
24+
expect(platform).toBeTruthy();
25+
expect(arch).toBeTruthy();
26+
});
27+
});
28+
29+
describe('User-Agent Generation', () => {
30+
test('should generate valid User-Agent format', () => {
31+
const userAgent = generateUserAgent();
32+
expect(userAgent).toMatch(/^QwenCode\/\d+\.\d+\.\d+ \(.+; .+\)$/);
33+
});
34+
35+
test('should include version 0.12.0', () => {
36+
const userAgent = generateUserAgent();
37+
expect(userAgent).toContain('QwenCode/0.12.0');
38+
});
39+
40+
test('should include detected platform', () => {
41+
const userAgent = generateUserAgent();
42+
const platform = detectPlatform();
43+
expect(userAgent).toContain(platform);
44+
});
45+
46+
test('should include detected architecture', () => {
47+
const userAgent = generateUserAgent();
48+
const arch = detectArch();
49+
expect(userAgent).toContain(arch);
50+
});
51+
});
52+
53+
describe('Platform Mapping', () => {
54+
test('should map known platforms correctly', () => {
55+
// This test verifies the mapping logic works
56+
// Actual values depend on the runtime environment
57+
const platform = detectPlatform();
58+
const knownPlatforms = ['Linux', 'macOS', 'Windows', 'FreeBSD', 'OpenBSD', 'Solaris', 'AIX', 'Unknown'];
59+
expect(knownPlatforms).toContain(platform);
60+
});
61+
62+
test('should map known architectures correctly', () => {
63+
const arch = detectArch();
64+
const knownArches = ['x64', 'arm64', 'ia32', 'ppc64', 'arm', 'mips', 'unknown'];
65+
expect(knownArches).toContain(arch);
66+
});
67+
});

0 commit comments

Comments
 (0)