From a3f1e52ac68600ba0806d67aacceb6477ffa3543 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Mon, 1 Dec 2025 03:51:47 -0500 Subject: [PATCH] feat(cliproxy): add qwen code oauth provider support (#31) Add 'qwen' as new CLIProxy OAuth provider with --qwen-login flag. Support qwen3-coder model with base config at config/base-qwen.settings.json. Register qwen OAuth endpoints and token prefix handling. Update help documentation across all CLI entry points. Implements issue #29 --- config/base-qwen.settings.json | 10 ++++++++++ src/auth/profile-detector.ts | 8 ++++---- src/ccs.ts | 2 +- src/cliproxy/auth-handler.ts | 11 ++++++++++- src/cliproxy/cliproxy-executor.ts | 2 +- src/cliproxy/config-generator.ts | 3 ++- src/cliproxy/types.ts | 3 ++- src/commands/help-command.ts | 1 + src/management/doctor.ts | 2 +- src/types/config.ts | 4 ++-- 10 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 config/base-qwen.settings.json diff --git a/config/base-qwen.settings.json b/config/base-qwen.settings.json new file mode 100644 index 00000000..03b3e36c --- /dev/null +++ b/config/base-qwen.settings.json @@ -0,0 +1,10 @@ +{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/qwen", + "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", + "ANTHROPIC_MODEL": "qwen3-coder", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3-coder", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-coder" + } +} diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index 70880391..2886090d 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -14,7 +14,7 @@ import { Config, Settings, ProfileMetadata } from '../types'; export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'default'; /** CLIProxy profile names (OAuth-based, zero config) */ -export const CLIPROXY_PROFILES = ['gemini', 'codex', 'agy'] as const; +export const CLIPROXY_PROFILES = ['gemini', 'codex', 'agy', 'qwen'] as const; export type CLIProxyProfileName = (typeof CLIPROXY_PROFILES)[number]; export interface ProfileDetectionResult { @@ -23,7 +23,7 @@ export interface ProfileDetectionResult { settingsPath?: string; profile?: Settings | ProfileMetadata; message?: string; - /** For cliproxy variants: the underlying provider (gemini, codex, agy) */ + /** For cliproxy variants: the underlying provider (gemini, codex, agy, qwen) */ provider?: CLIProxyProfileName; } @@ -89,7 +89,7 @@ class ProfileDetector { * Detect profile type and return routing information * * Priority order: - * 0. Hardcoded CLIProxy profiles (gemini, codex, agy) + * 0. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen) * 1. User-defined CLIProxy variants (config.cliproxy section) * 2. Settings-based profiles (config.profiles section) * 3. Account-based profiles (profiles.json) @@ -100,7 +100,7 @@ class ProfileDetector { return this.resolveDefaultProfile(); } - // Priority 0: Check CLIProxy profiles (gemini, codex, agy) - OAuth-based, zero config + // Priority 0: Check CLIProxy profiles (gemini, codex, agy, qwen) - OAuth-based, zero config if (CLIPROXY_PROFILES.includes(profileName as CLIProxyProfileName)) { return { type: 'cliproxy', diff --git a/src/ccs.ts b/src/ccs.ts index 231e5741..54c63046 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -295,7 +295,7 @@ async function main(): Promise { const profileInfo = detector.detectProfileType(profile); if (profileInfo.type === 'cliproxy') { - // CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy) or user-defined variants + // CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy, qwen) or user-defined variants const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider); const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, { customSettingsPath }); diff --git a/src/cliproxy/auth-handler.ts b/src/cliproxy/auth-handler.ts index 29de6b71..22cbabe6 100644 --- a/src/cliproxy/auth-handler.ts +++ b/src/cliproxy/auth-handler.ts @@ -100,6 +100,13 @@ const OAUTH_CONFIGS: Record = { scopes: ['api'], authFlag: '--antigravity-login', }, + qwen: { + provider: 'qwen', + displayName: 'Qwen Code', + authUrl: 'https://chat.qwen.ai/api/v1/oauth2/device/code', + scopes: ['openid', 'profile', 'email', 'model.completion'], + authFlag: '--qwen-login', + }, }; /** @@ -129,6 +136,7 @@ const PROVIDER_AUTH_PREFIXES: Record = { gemini: ['gemini-', 'google-'], codex: ['codex-', 'openai-'], agy: ['antigravity-', 'agy-'], + qwen: ['qwen-'], }; /** @@ -139,6 +147,7 @@ const PROVIDER_TYPE_VALUES: Record = { gemini: ['gemini'], codex: ['codex'], agy: ['antigravity'], + qwen: ['qwen'], }; /** @@ -256,7 +265,7 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus { * Get auth status for all providers */ export function getAllAuthStatus(): AuthStatus[] { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy']; + const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen']; return providers.map(getAuthStatus); } diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index d95b839a..695adace 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -80,7 +80,7 @@ async function waitForProxyReady( * Execute Claude CLI with CLIProxy (main entry point) * * @param claudeCli Path to Claude CLI executable - * @param provider CLIProxy provider (gemini, codex, agy) + * @param provider CLIProxy provider (gemini, codex, agy, qwen) * @param args Arguments to pass to Claude CLI * @param config Optional executor configuration */ diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index e7e4ed64..11354a1e 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -30,6 +30,7 @@ const PROVIDER_DISPLAY_NAMES: Record = { gemini: 'Gemini', codex: 'Codex', agy: 'Antigravity', + qwen: 'Qwen Code', }; /** @@ -112,7 +113,7 @@ function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): str // Unified config with all providers const config = `# CLIProxyAPI unified config generated by CCS -# Supports: gemini, codex, agy (concurrent usage) +# Supports: gemini, codex, agy, qwen (concurrent usage) # Generated: ${new Date().toISOString()} port: ${port} diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index d83abf2f..86c20be7 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -110,8 +110,9 @@ export interface DownloadResult { * - gemini: Google Gemini via OAuth * - codex: OpenAI Codex via OAuth * - agy: Antigravity via OAuth (short name for easy usage) + * - qwen: Qwen Code via OAuth (qwen3-coder) */ -export type CLIProxyProvider = 'gemini' | 'codex' | 'agy'; +export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen'; /** * CLIProxy config.yaml structure (minimal) diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 3cecbd09..07b406d5 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -54,6 +54,7 @@ export function handleHelpCommand(): void { console.log( ` ${colored('ccs agy', 'yellow')} Antigravity (gemini-3-pro-preview)` ); + console.log(` ${colored('ccs qwen', 'yellow')} Qwen Code (qwen3-coder)`); console.log(''); console.log(` ${colored('ccs --auth', 'yellow')} Authenticate only`); console.log(` ${colored('ccs --logout', 'yellow')} Clear authentication`); diff --git a/src/management/doctor.ts b/src/management/doctor.ts index 34d878b0..17bb0366 100644 --- a/src/management/doctor.ts +++ b/src/management/doctor.ts @@ -754,7 +754,7 @@ class Doctor { } /** - * Check 11: CLIProxy health (OAuth profiles: gemini, codex, agy) + * Check 11: CLIProxy health (OAuth profiles: gemini, codex, agy, qwen) */ private async checkCLIProxy(): Promise { // 1. Binary installed? diff --git a/src/types/config.ts b/src/types/config.ts index 5b0689c6..decad645 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -17,8 +17,8 @@ export interface ProfilesConfig { * Example: "flash" → gemini provider with gemini-2.5-flash model */ export interface CLIProxyVariantConfig { - /** CLIProxy provider to use (gemini, codex, agy) */ - provider: 'gemini' | 'codex' | 'agy'; + /** CLIProxy provider to use (gemini, codex, agy, qwen) */ + provider: 'gemini' | 'codex' | 'agy' | 'qwen'; /** Path to settings.json with custom model configuration */ settings: string; }