chore: promote beta to main (v5.2.0) (#32)

* fix(cliproxy): use double-dash flags for cliproxyapi auth (#24)

CLIProxyAPI updated from single-dash to double-dash CLI flags.
Changed -login to --login, -codex-login to --codex-login,
-antigravity-login to --antigravity-login, -config to --config,
and -no-browser to --no-browser.

* chore(release): 5.1.1-beta.1 [skip ci]

## [5.1.1-beta.1](https://github.com/kaitranntt/ccs/compare/v5.1.0...v5.1.1-beta.1) (2025-12-01)

### Bug Fixes

* **cliproxy:** use double-dash flags for cliproxyapi auth ([#24](https://github.com/kaitranntt/ccs/issues/24)) ([4c81f28](https://github.com/kaitranntt/ccs/commit/4c81f28f0b67ef92cf74d0f5c13a5943ff0a7f00))

* chore(release): 5.1.1-beta.2 [skip ci]

## [5.1.1-beta.2](https://github.com/kaitranntt/ccs/compare/v5.1.1-beta.1...v5.1.1-beta.2) (2025-12-01)

### Bug Fixes

* **cliproxy:** use double-dash flags for cliproxyapi auth ([9489884](https://github.com/kaitranntt/ccs/commit/94898848ea4533dcfc142e1b6c9bf939ba655537))

* fix(glmt): handle 401 errors and headers-already-sent exception (#30)

- add res.headersSent check before writing error headers
- write error as SSE event when headers already sent (streaming mode)
- add parseUpstreamError() for user-friendly error messages
- return proper HTTP status codes (401, 403, 429, 502, 504)
- clear auth error: check ANTHROPIC_AUTH_TOKEN in config

Fixes #26

* chore(release): 5.1.1-beta.3 [skip ci]

## [5.1.1-beta.3](https://github.com/kaitranntt/ccs/compare/v5.1.1-beta.2...v5.1.1-beta.3) (2025-12-01)

### Bug Fixes

* **glmt:** handle 401 errors and headers-already-sent exception ([#30](https://github.com/kaitranntt/ccs/issues/30)) ([c953382](https://github.com/kaitranntt/ccs/commit/c95338232a37981b95b785b47185ce18d6d94b7a)), closes [#26](https://github.com/kaitranntt/ccs/issues/26)

* chore(release): enable success comments and released labels in semantic-release

* 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

* chore(release): 5.2.0-beta.1 [skip ci]

# [5.2.0-beta.1](https://github.com/kaitranntt/ccs/compare/v5.1.1-beta.3...v5.2.0-beta.1) (2025-12-01)

### Features

* **cliproxy:** add qwen code oauth provider support ([#31](https://github.com/kaitranntt/ccs/issues/31)) ([a3f1e52](https://github.com/kaitranntt/ccs/commit/a3f1e52ac68600ba0806d67aacceb6477ffa3543)), closes [#29](https://github.com/kaitranntt/ccs/issues/29)

* feat(cliproxy): auto-update cliproxyapi to latest release on startup

- add github api integration to fetch latest release version
- check for updates on every startup (cached for 1 hour)
- auto-download newer versions when available
- rename CLIPROXY_VERSION to CLIPROXY_FALLBACK_VERSION (safety net)
- add version tracking (.version file) and caching (.version-cache.json)
- non-blocking: startup continues if update check fails

* chore(release): 5.2.0-beta.2 [skip ci]

# [5.2.0-beta.2](https://github.com/kaitranntt/ccs/compare/v5.2.0-beta.1...v5.2.0-beta.2) (2025-12-01)

### Features

* **cliproxy:** auto-update cliproxyapi to latest release on startup ([8873ccd](https://github.com/kaitranntt/ccs/commit/8873ccd981679e8acff8965accdc22215c6e4aa2))

---------

Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
This commit is contained in:
Kai (Tam Nhu) Tran
2025-12-01 04:11:14 -05:00
committed by GitHub
co-authored by semantic-release-bot
parent 27ee769ec0
commit 7fc281fcec
14 changed files with 399 additions and 32 deletions
+3 -2
View File
@@ -18,8 +18,9 @@
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}],
["@semantic-release/github", {
"successComment": false,
"failComment": false
"successComment": ":tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on:\n- [npm package (@${nextRelease.channel || 'latest'})](https://www.npmjs.com/package/ccs-claude-code-switcher)\n- [GitHub release](${releases.filter(r => r.name === 'GitHub release').map(r => r.url)[0] || url})",
"failComment": false,
"releasedLabels": ["released"]
}]
]
}
+10
View File
@@ -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"
}
}
+4 -4
View File
@@ -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',
+1 -1
View File
@@ -295,7 +295,7 @@ async function main(): Promise<void> {
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 });
+10 -1
View File
@@ -100,6 +100,13 @@ const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
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<CLIProxyProvider, string[]> = {
gemini: ['gemini-', 'google-'],
codex: ['codex-', 'openai-'],
agy: ['antigravity-', 'agy-'],
qwen: ['qwen-'],
};
/**
@@ -139,6 +147,7 @@ const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = {
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);
}
+256 -6
View File
@@ -6,6 +6,7 @@
* - Verifies SHA256 checksum
* - Extracts and caches binary locally
* - Supports retry logic with exponential backoff
* - Auto-checks for updates on startup (fetches latest from GitHub API)
*
* Pattern: Mirrors npm install behavior (fast check, download only when needed)
*/
@@ -17,7 +18,7 @@ import * as http from 'http';
import * as crypto from 'crypto';
import * as zlib from 'zlib';
import { ProgressIndicator } from '../utils/progress-indicator';
import { getBinDir } from './config-generator';
import { getBinDir, getCliproxyDir } from './config-generator';
import {
BinaryInfo,
BinaryManagerConfig,
@@ -31,12 +32,33 @@ import {
getChecksumsUrl,
getExecutableName,
getArchiveBinaryName,
CLIPROXY_VERSION,
CLIPROXY_FALLBACK_VERSION,
} from './platform-detector';
/** Cache duration for version check (1 hour in milliseconds) */
const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000;
/** GitHub API URL for latest release */
const GITHUB_API_LATEST_RELEASE =
'https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest';
/** Version cache file structure */
interface VersionCache {
latestVersion: string;
checkedAt: number;
}
/** Update check result */
interface UpdateCheckResult {
hasUpdate: boolean;
currentVersion: string;
latestVersion: string;
fromCache: boolean;
}
/** Default configuration */
const DEFAULT_CONFIG: BinaryManagerConfig = {
version: CLIPROXY_VERSION,
version: CLIPROXY_FALLBACK_VERSION,
releaseUrl: 'https://github.com/router-for-me/CLIProxyAPI/releases/download',
binPath: getBinDir(),
maxRetries: 3,
@@ -56,7 +78,7 @@ export class BinaryManager {
}
/**
* Ensure binary is available (download if missing)
* Ensure binary is available (download if missing, update if outdated)
* @returns Path to executable binary
*/
async ensureBinary(): Promise<string> {
@@ -65,16 +87,241 @@ export class BinaryManager {
// Check if binary already exists
if (fs.existsSync(binaryPath)) {
this.log(`Binary exists: ${binaryPath}`);
return binaryPath;
// Check for updates in background (non-blocking for UX)
try {
const updateResult = await this.checkForUpdates();
if (updateResult.hasUpdate) {
console.log(
`[i] CLIProxyAPI update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}`
);
console.log(`[i] Updating CLIProxyAPI...`);
// Delete old binary and download new version
this.deleteBinary();
this.config.version = updateResult.latestVersion;
await this.downloadAndInstall();
}
} catch (error) {
// Silent fail - don't block startup if update check fails
const err = error as Error;
this.log(`Update check failed (non-blocking): ${err.message}`);
}
return this.getBinaryPath();
}
// Download, verify, extract
this.log('Binary not found, downloading...');
// Check latest version before first download
try {
const latestVersion = await this.fetchLatestVersion();
if (latestVersion && this.isNewerVersion(latestVersion, this.config.version)) {
this.log(`Using latest version: ${latestVersion} (instead of ${this.config.version})`);
this.config.version = latestVersion;
}
} catch {
// Use pinned version if API fails
this.log(`Using pinned version: ${this.config.version}`);
}
await this.downloadAndInstall();
return binaryPath;
}
/**
* Check for updates by comparing installed version with latest release
* Uses cache to avoid hitting GitHub API on every run
*/
async checkForUpdates(): Promise<UpdateCheckResult> {
const currentVersion = this.getInstalledVersion();
// Try cache first
const cachedVersion = this.getCachedLatestVersion();
if (cachedVersion) {
this.log(`Using cached version: ${cachedVersion}`);
return {
hasUpdate: this.isNewerVersion(cachedVersion, currentVersion),
currentVersion,
latestVersion: cachedVersion,
fromCache: true,
};
}
// Fetch from GitHub API
const latestVersion = await this.fetchLatestVersion();
this.cacheLatestVersion(latestVersion);
return {
hasUpdate: this.isNewerVersion(latestVersion, currentVersion),
currentVersion,
latestVersion,
fromCache: false,
};
}
/**
* Fetch latest version from GitHub API
*/
private async fetchLatestVersion(): Promise<string> {
const response = await this.fetchJson(GITHUB_API_LATEST_RELEASE);
// Extract version from tag_name (format: "v6.5.27" or "6.5.27")
const tagName = response.tag_name as string;
if (!tagName) {
throw new Error('No tag_name in GitHub API response');
}
return tagName.replace(/^v/, '');
}
/**
* Fetch JSON from URL (for GitHub API)
*/
private fetchJson(url: string): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
const options = {
headers: {
'User-Agent': 'CCS-CLIProxyAPI-Updater/1.0',
Accept: 'application/vnd.github.v3+json',
},
};
const handleResponse = (res: http.IncomingMessage) => {
if (res.statusCode === 301 || res.statusCode === 302) {
const redirectUrl = res.headers.location;
if (!redirectUrl) {
reject(new Error('Redirect without location header'));
return;
}
this.fetchJson(redirectUrl).then(resolve).catch(reject);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`GitHub API error: HTTP ${res.statusCode}`));
return;
}
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch {
reject(new Error('Invalid JSON from GitHub API'));
}
});
res.on('error', reject);
};
const req = https.get(url, options, handleResponse);
req.on('error', reject);
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('GitHub API timeout (10s)'));
});
});
}
/**
* Get installed version from version file or fallback to pinned
*/
private getInstalledVersion(): string {
const versionFile = path.join(this.config.binPath, '.version');
if (fs.existsSync(versionFile)) {
try {
return fs.readFileSync(versionFile, 'utf8').trim();
} catch {
return this.config.version;
}
}
return this.config.version;
}
/**
* Save installed version to file
*/
private saveInstalledVersion(version: string): void {
const versionFile = path.join(this.config.binPath, '.version');
try {
fs.writeFileSync(versionFile, version, 'utf8');
} catch {
// Silent fail - not critical
}
}
/**
* Get cached latest version if still valid
*/
private getCachedLatestVersion(): string | null {
const cachePath = this.getVersionCachePath();
if (!fs.existsSync(cachePath)) {
return null;
}
try {
const content = fs.readFileSync(cachePath, 'utf8');
const cache: VersionCache = JSON.parse(content);
// Check if cache is still valid
if (Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
return cache.latestVersion;
}
// Cache expired
return null;
} catch {
return null;
}
}
/**
* Cache latest version for future checks
*/
private cacheLatestVersion(version: string): void {
const cachePath = this.getVersionCachePath();
const cache: VersionCache = {
latestVersion: version,
checkedAt: Date.now(),
};
try {
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify(cache), 'utf8');
} catch {
// Silent fail - caching is optional
}
}
/**
* Get path to version cache file
*/
private getVersionCachePath(): string {
return path.join(getCliproxyDir(), '.version-cache.json');
}
/**
* Compare semver versions (true if latest > current)
*/
private isNewerVersion(latest: string, current: string): boolean {
const latestParts = latest.split('.').map((p) => parseInt(p, 10) || 0);
const currentParts = current.split('.').map((p) => parseInt(p, 10) || 0);
// Pad arrays to same length
while (latestParts.length < 3) latestParts.push(0);
while (currentParts.length < 3) currentParts.push(0);
for (let i = 0; i < 3; i++) {
if (latestParts[i] > currentParts[i]) return true;
if (latestParts[i] < currentParts[i]) return false;
}
return false; // Equal versions
}
/**
* Get full path to binary executable
*/
@@ -114,7 +361,7 @@ export class BinaryManager {
* Download and install binary
*/
private async downloadAndInstall(): Promise<void> {
const platform = detectPlatform();
const platform = detectPlatform(this.config.version);
const downloadUrl = getDownloadUrl(this.config.version);
const checksumsUrl = getChecksumsUrl(this.config.version);
@@ -178,6 +425,9 @@ export class BinaryManager {
this.log(`Set executable permissions: ${binaryPath}`);
}
// Save installed version for future update checks
this.saveInstalledVersion(this.config.version);
console.log(`[OK] CLIProxyAPI v${this.config.version} installed successfully`);
} catch (error) {
spinner.fail('Installation failed');
+1 -1
View File
@@ -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
*/
+2 -1
View File
@@ -30,6 +30,7 @@ const PROVIDER_DISPLAY_NAMES: Record<CLIProxyProvider, string> = {
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}
+16 -9
View File
@@ -7,8 +7,14 @@
import { PlatformInfo, SupportedOS, SupportedArch, ArchiveExtension } from './types';
/** CLIProxyAPI version pinned for stability */
export const CLIPROXY_VERSION = '6.5.27';
/**
* CLIProxyAPI fallback version (used when GitHub API unavailable)
* Auto-update fetches latest from GitHub; this is only a safety net
*/
export const CLIPROXY_FALLBACK_VERSION = '6.5.27';
/** @deprecated Use CLIPROXY_FALLBACK_VERSION instead */
export const CLIPROXY_VERSION = CLIPROXY_FALLBACK_VERSION;
/**
* Platform mapping from Node.js values to CLIProxyAPI naming
@@ -26,9 +32,10 @@ const ARCH_MAP: Record<string, SupportedArch | undefined> = {
/**
* Detect current platform and return binary info
* @param version Optional version for binaryName (defaults to fallback)
* @throws Error if platform is unsupported
*/
export function detectPlatform(): PlatformInfo {
export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): PlatformInfo {
const nodePlatform = process.platform;
const nodeArch = process.arch;
@@ -49,7 +56,7 @@ export function detectPlatform(): PlatformInfo {
}
const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz';
const binaryName = `CLIProxyAPI_${CLIPROXY_VERSION}_${os}_${arch}.${extension}`;
const binaryName = `CLIProxyAPI_${version}_${os}_${arch}.${extension}`;
return {
os,
@@ -80,21 +87,21 @@ export function getArchiveBinaryName(): string {
/**
* Get download URL for current platform
* @param version Optional version override (defaults to CLIPROXY_VERSION)
* @param version Version to download (defaults to fallback version)
* @returns Full GitHub release download URL
*/
export function getDownloadUrl(version: string = CLIPROXY_VERSION): string {
const platform = detectPlatform();
export function getDownloadUrl(version: string = CLIPROXY_FALLBACK_VERSION): string {
const platform = detectPlatform(version);
const baseUrl = `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${version}`;
return `${baseUrl}/${platform.binaryName}`;
}
/**
* Get checksums.txt URL for version
* @param version Optional version override
* @param version Version to get checksums for (defaults to fallback version)
* @returns Full URL to checksums.txt
*/
export function getChecksumsUrl(version: string = CLIPROXY_VERSION): string {
export function getChecksumsUrl(version: string = CLIPROXY_FALLBACK_VERSION): string {
return `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${version}/checksums.txt`;
}
+2 -1
View File
@@ -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)
+1
View File
@@ -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 <provider> --auth', 'yellow')} Authenticate only`);
console.log(` ${colored('ccs <provider> --logout', 'yellow')} Clear authentication`);
+90 -3
View File
@@ -208,12 +208,25 @@ export class GlmtProxy {
const duration = Date.now() - startTime;
this.log(`Request failed after ${duration}ms: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'application/json' });
// Parse error to provide clearer messages
const errorInfo = this.parseUpstreamError(err);
// Check if headers already sent (streaming mode may have started)
if (res.headersSent) {
// Headers already sent, write error as SSE event and close
this.log('Headers already sent, writing error as SSE event');
res.write(`event: error\n`);
res.write(`data: ${JSON.stringify({ error: errorInfo })}\n\n`);
res.end();
return;
}
res.writeHead(errorInfo.statusCode, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
error: {
type: 'proxy_error',
message: err.message,
type: errorInfo.type,
message: errorInfo.message,
},
})
);
@@ -526,6 +539,80 @@ export class GlmtProxy {
console.error(`[glmt-proxy] ${message}`);
}
}
/**
* Parse upstream error and return user-friendly error info
*/
private parseUpstreamError(err: Error): {
statusCode: number;
type: string;
message: string;
} {
const errorMessage = err.message;
// Check for 401 Unauthorized / auth token missing
if (
errorMessage.includes('401') ||
errorMessage.toLowerCase().includes('unauthorized') ||
errorMessage.toLowerCase().includes('authorization token missing') ||
errorMessage.toLowerCase().includes('invalid token')
) {
return {
statusCode: 401,
type: 'authentication_error',
message:
'Authorization token missing or invalid. Please check your ANTHROPIC_AUTH_TOKEN environment variable in ~/.ccs/config.json or settings.json.',
};
}
// Check for 403 Forbidden
if (errorMessage.includes('403') || errorMessage.toLowerCase().includes('forbidden')) {
return {
statusCode: 403,
type: 'permission_error',
message: 'Access forbidden. Your token may not have permission to access this resource.',
};
}
// Check for 429 Rate limit
if (errorMessage.includes('429') || errorMessage.toLowerCase().includes('rate limit')) {
return {
statusCode: 429,
type: 'rate_limit_error',
message: 'Rate limit exceeded. Please wait before making more requests.',
};
}
// Check for timeout
if (errorMessage.toLowerCase().includes('timeout')) {
return {
statusCode: 504,
type: 'timeout_error',
message: 'Request timed out. The upstream server took too long to respond.',
};
}
// Check for connection errors
if (
errorMessage.toLowerCase().includes('econnrefused') ||
errorMessage.toLowerCase().includes('enotfound') ||
errorMessage.toLowerCase().includes('connection')
) {
return {
statusCode: 502,
type: 'connection_error',
message:
'Failed to connect to upstream server. Please check your network and ANTHROPIC_BASE_URL configuration.',
};
}
// Default: proxy error
return {
statusCode: 500,
type: 'proxy_error',
message: errorMessage,
};
}
}
// Main entry point
+1 -1
View File
@@ -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<void> {
// 1. Binary installed?
+2 -2
View File
@@ -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;
}