mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
Merge pull request #347 from kaitranntt/feat/cliproxy-backend-selection
feat(cliproxy): add backend selection for CLIProxyAPI vs CLIProxyAPIPlus
This commit is contained in:
@@ -8,9 +8,10 @@
|
||||
import { info, warn } from '../utils/ui';
|
||||
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
|
||||
import { BinaryInfo, BinaryManagerConfig } from './types';
|
||||
import { CLIPROXY_FALLBACK_VERSION, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
|
||||
import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
|
||||
import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service';
|
||||
import { waitForPortFree } from '../utils/port-utils';
|
||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||
import {
|
||||
UpdateCheckResult,
|
||||
checkForUpdates,
|
||||
@@ -27,24 +28,53 @@ import {
|
||||
ensureBinary,
|
||||
} from './binary';
|
||||
|
||||
/** Default configuration (uses CLIProxyAPIPlus fork with Kiro + Copilot support) */
|
||||
const DEFAULT_CONFIG: BinaryManagerConfig = {
|
||||
version: CLIPROXY_FALLBACK_VERSION,
|
||||
releaseUrl: 'https://github.com/router-for-me/CLIProxyAPIPlus/releases/download',
|
||||
binPath: getBinDir(),
|
||||
maxRetries: 3,
|
||||
verbose: false,
|
||||
forceVersion: false,
|
||||
};
|
||||
type CLIProxyBackend = 'original' | 'plus';
|
||||
|
||||
/**
|
||||
* Get backend from config or default to 'plus'
|
||||
*/
|
||||
function getConfiguredBackend(): CLIProxyBackend {
|
||||
try {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return config.cliproxy?.backend || DEFAULT_BACKEND;
|
||||
} catch {
|
||||
return DEFAULT_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backend-specific binary directory.
|
||||
* Stores binaries in separate dirs: bin/original/ and bin/plus/
|
||||
*/
|
||||
function getBackendBinDir(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
const baseDir = getBinDir();
|
||||
return `${baseDir}/${backend}`;
|
||||
}
|
||||
|
||||
/** Default configuration (uses backend from config.yaml or defaults to 'plus') */
|
||||
function createDefaultConfig(backend: CLIProxyBackend = DEFAULT_BACKEND): BinaryManagerConfig {
|
||||
const backendConfig = BACKEND_CONFIG[backend];
|
||||
return {
|
||||
version: backendConfig.fallbackVersion,
|
||||
releaseUrl: `https://github.com/${backendConfig.repo}/releases/download`,
|
||||
binPath: getBackendBinDir(backend),
|
||||
maxRetries: 3,
|
||||
verbose: false,
|
||||
forceVersion: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary Manager class for CLIProxyAPI binary lifecycle
|
||||
*/
|
||||
export class BinaryManager {
|
||||
private config: BinaryManagerConfig;
|
||||
private backend: CLIProxyBackend;
|
||||
|
||||
constructor(config: Partial<BinaryManagerConfig> = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
|
||||
this.backend = backend ?? getConfiguredBackend();
|
||||
const defaultConfig = createDefaultConfig(this.backend);
|
||||
this.config = { ...defaultConfig, ...config };
|
||||
}
|
||||
|
||||
/** Ensure binary is available (download if missing, update if outdated) */
|
||||
@@ -80,36 +110,44 @@ export class BinaryManager {
|
||||
|
||||
/** Convenience function respecting version pin */
|
||||
export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
||||
const backend = getConfiguredBackend();
|
||||
const pinnedVersion = getPinnedVersion();
|
||||
if (pinnedVersion) {
|
||||
if (verbose) console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`);
|
||||
return new BinaryManager({
|
||||
version: pinnedVersion,
|
||||
verbose,
|
||||
forceVersion: true,
|
||||
}).ensureBinary();
|
||||
return new BinaryManager(
|
||||
{
|
||||
version: pinnedVersion,
|
||||
verbose,
|
||||
forceVersion: true,
|
||||
},
|
||||
backend
|
||||
).ensureBinary();
|
||||
}
|
||||
return new BinaryManager({ verbose }).ensureBinary();
|
||||
return new BinaryManager({ verbose }, backend).ensureBinary();
|
||||
}
|
||||
|
||||
/** Check if CLIProxyAPI binary is installed */
|
||||
export function isCLIProxyInstalled(): boolean {
|
||||
return new BinaryManager().isBinaryInstalled();
|
||||
const backend = getConfiguredBackend();
|
||||
return new BinaryManager({}, backend).isBinaryInstalled();
|
||||
}
|
||||
|
||||
/** Get CLIProxyAPI binary path (may not exist) */
|
||||
export function getCLIProxyPath(): string {
|
||||
return new BinaryManager().getBinaryPath();
|
||||
const backend = getConfiguredBackend();
|
||||
return new BinaryManager({}, backend).getBinaryPath();
|
||||
}
|
||||
|
||||
/** Get installed CLIProxyAPI version from .version file */
|
||||
export function getInstalledCliproxyVersion(): string {
|
||||
return readInstalledVersion(getBinDir(), CLIPROXY_FALLBACK_VERSION);
|
||||
const backend = getConfiguredBackend();
|
||||
return readInstalledVersion(getBackendBinDir(backend), BACKEND_CONFIG[backend].fallbackVersion);
|
||||
}
|
||||
|
||||
/** Install a specific version of CLIProxyAPI */
|
||||
export async function installCliproxyVersion(version: string, verbose = false): Promise<void> {
|
||||
const manager = new BinaryManager({ version, verbose, forceVersion: true });
|
||||
const backend = getConfiguredBackend();
|
||||
const manager = new BinaryManager({ version, verbose, forceVersion: true }, backend);
|
||||
|
||||
// Check if proxy is running and stop it first
|
||||
if (isProxyRunning()) {
|
||||
@@ -140,7 +178,8 @@ export async function installCliproxyVersion(version: string, verbose = false):
|
||||
|
||||
/** Fetch the latest CLIProxyAPI version from GitHub API */
|
||||
export async function fetchLatestCliproxyVersion(): Promise<string> {
|
||||
const result = await new BinaryManager().checkForUpdates();
|
||||
const backend = getConfiguredBackend();
|
||||
const result = await new BinaryManager({}, backend).checkForUpdates();
|
||||
return result.latestVersion;
|
||||
}
|
||||
|
||||
@@ -159,7 +198,8 @@ export interface CliproxyUpdateCheckResult {
|
||||
|
||||
/** Check for CLIProxyAPI binary updates */
|
||||
export async function checkCliproxyUpdate(): Promise<CliproxyUpdateCheckResult> {
|
||||
const result = await new BinaryManager().checkForUpdates();
|
||||
const backend = getConfiguredBackend();
|
||||
const result = await new BinaryManager({}, backend).checkForUpdates();
|
||||
|
||||
// Import isNewerVersion for stability check
|
||||
const { isNewerVersion } = await import('./binary/version-checker');
|
||||
|
||||
@@ -30,7 +30,8 @@ import {
|
||||
} from './config-generator';
|
||||
import { checkRemoteProxy } from './remote-proxy-client';
|
||||
import { isAuthenticated } from './auth-handler';
|
||||
import { CLIProxyProvider, ExecutorConfig } from './types';
|
||||
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from './types';
|
||||
import { DEFAULT_BACKEND } from './platform-detector';
|
||||
import { configureProviderModel, getCurrentModel } from './model-config';
|
||||
import { resolveProxyConfig, PROXY_CLI_FLAGS } from './proxy-config-resolver';
|
||||
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
||||
@@ -141,6 +142,20 @@ export async function execClaudeWithCLIProxy(
|
||||
// 0. Resolve proxy configuration (CLI > ENV > config.yaml > defaults)
|
||||
// This filters proxy flags from args and returns resolved config
|
||||
const unifiedConfig = loadOrCreateUnifiedConfig();
|
||||
|
||||
// 0a. Runtime backend/provider validation - block kiro/ghcp if backend=original
|
||||
const backend: CLIProxyBackend = unifiedConfig.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(provider)) {
|
||||
console.error('');
|
||||
console.error(fail(`${provider} requires CLIProxyAPIPlus backend`));
|
||||
console.error('');
|
||||
console.error('To use this provider, either:');
|
||||
console.error(' 1. Set `cliproxy.backend: plus` in ~/.ccs/config.yaml');
|
||||
console.error(' 2. Use --backend=plus flag: ccs ' + provider + ' --backend=plus');
|
||||
console.error('');
|
||||
throw new Error(`Provider ${provider} requires Plus backend`);
|
||||
}
|
||||
|
||||
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
|
||||
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
|
||||
remote: cliproxyServerConfig?.remote
|
||||
|
||||
@@ -5,14 +5,40 @@
|
||||
* Supports 6 platforms: darwin/linux/windows x amd64/arm64
|
||||
*/
|
||||
|
||||
import { PlatformInfo, SupportedOS, SupportedArch, ArchiveExtension } from './types';
|
||||
import {
|
||||
PlatformInfo,
|
||||
SupportedOS,
|
||||
SupportedArch,
|
||||
ArchiveExtension,
|
||||
CLIProxyBackend,
|
||||
} from './types';
|
||||
|
||||
/** Backend configuration */
|
||||
export const BACKEND_CONFIG = {
|
||||
original: {
|
||||
repo: 'router-for-me/CLIProxyAPI',
|
||||
binaryPrefix: 'CLIProxyAPI',
|
||||
executable: 'cli-proxy-api',
|
||||
fallbackVersion: '6.7.8',
|
||||
},
|
||||
plus: {
|
||||
repo: 'router-for-me/CLIProxyAPIPlus',
|
||||
binaryPrefix: 'CLIProxyAPIPlus',
|
||||
executable: 'cli-proxy-api-plus',
|
||||
fallbackVersion: '6.7.8-0',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** Default backend */
|
||||
export const DEFAULT_BACKEND: CLIProxyBackend = 'plus';
|
||||
|
||||
/**
|
||||
* CLIProxyAPIPlus fallback version (used when GitHub API unavailable)
|
||||
* Auto-update fetches latest from GitHub; this is only a safety net
|
||||
* Note: CLIProxyAPIPlus uses v6.6.X-0 suffix pattern
|
||||
* @deprecated Use getFallbackVersion() or BACKEND_CONFIG instead
|
||||
*/
|
||||
export const CLIPROXY_FALLBACK_VERSION = '6.6.40-0';
|
||||
export const CLIPROXY_FALLBACK_VERSION = BACKEND_CONFIG[DEFAULT_BACKEND].fallbackVersion;
|
||||
|
||||
/**
|
||||
* Maximum stable version cap - prevents auto-update to known unstable releases
|
||||
@@ -48,10 +74,14 @@ const ARCH_MAP: Record<string, SupportedArch | undefined> = {
|
||||
|
||||
/**
|
||||
* Detect current platform and return binary info
|
||||
* @param version Optional version for binaryName (defaults to fallback)
|
||||
* @param version Optional version for binaryName (defaults to backend fallback)
|
||||
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||
* @throws Error if platform is unsupported
|
||||
*/
|
||||
export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): PlatformInfo {
|
||||
export function detectPlatform(
|
||||
version?: string,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): PlatformInfo {
|
||||
const nodePlatform = process.platform;
|
||||
const nodeArch = process.arch;
|
||||
|
||||
@@ -71,8 +101,10 @@ export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): Pla
|
||||
);
|
||||
}
|
||||
|
||||
const config = BACKEND_CONFIG[backend];
|
||||
const ver = version || config.fallbackVersion;
|
||||
const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz';
|
||||
const binaryName = `CLIProxyAPIPlus_${version}_${os}_${arch}.${extension}`;
|
||||
const binaryName = `${config.binaryPrefix}_${ver}_${os}_${arch}.${extension}`;
|
||||
|
||||
return {
|
||||
os,
|
||||
@@ -84,41 +116,62 @@ export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): Pla
|
||||
|
||||
/**
|
||||
* Get executable name based on platform
|
||||
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||
* @returns Binary executable name (with .exe on Windows)
|
||||
* Note: The actual binary inside the archive is named 'cli-proxy-api-plus'
|
||||
*/
|
||||
export function getExecutableName(): string {
|
||||
const platform = detectPlatform();
|
||||
return platform.os === 'windows' ? 'cli-proxy-api-plus.exe' : 'cli-proxy-api-plus';
|
||||
export function getExecutableName(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
const config = BACKEND_CONFIG[backend];
|
||||
const platform = detectPlatform(undefined, backend);
|
||||
return platform.os === 'windows' ? `${config.executable}.exe` : config.executable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the binary inside the archive
|
||||
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||
* @returns Binary name as it appears in the tar.gz/zip
|
||||
*/
|
||||
export function getArchiveBinaryName(): string {
|
||||
const platform = detectPlatform();
|
||||
return platform.os === 'windows' ? 'cli-proxy-api-plus.exe' : 'cli-proxy-api-plus';
|
||||
export function getArchiveBinaryName(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
return getExecutableName(backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download URL for current platform
|
||||
* @param version Version to download (defaults to fallback version)
|
||||
* @param version Version to download (defaults to backend fallback version)
|
||||
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||
* @returns Full GitHub release download URL
|
||||
*/
|
||||
export function getDownloadUrl(version: string = CLIPROXY_FALLBACK_VERSION): string {
|
||||
const platform = detectPlatform(version);
|
||||
const baseUrl = `https://github.com/router-for-me/CLIProxyAPIPlus/releases/download/v${version}`;
|
||||
return `${baseUrl}/${platform.binaryName}`;
|
||||
export function getDownloadUrl(
|
||||
version?: string,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): string {
|
||||
const config = BACKEND_CONFIG[backend];
|
||||
const ver = version || config.fallbackVersion;
|
||||
const platform = detectPlatform(ver, backend);
|
||||
return `https://github.com/${config.repo}/releases/download/v${ver}/${platform.binaryName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checksums.txt URL for version
|
||||
* @param version Version to get checksums for (defaults to fallback version)
|
||||
* @param version Version to get checksums for (defaults to backend fallback version)
|
||||
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||
* @returns Full URL to checksums.txt
|
||||
*/
|
||||
export function getChecksumsUrl(version: string = CLIPROXY_FALLBACK_VERSION): string {
|
||||
return `https://github.com/router-for-me/CLIProxyAPIPlus/releases/download/v${version}/checksums.txt`;
|
||||
export function getChecksumsUrl(
|
||||
version?: string,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): string {
|
||||
const config = BACKEND_CONFIG[backend];
|
||||
const ver = version || config.fallbackVersion;
|
||||
return `https://github.com/${config.repo}/releases/download/v${ver}/checksums.txt`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fallback version for backend
|
||||
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||
* @returns Fallback version string
|
||||
*/
|
||||
export function getFallbackVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
return BACKEND_CONFIG[backend].fallbackVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
clearPinnedVersion,
|
||||
isVersionPinned,
|
||||
} from '../binary-manager';
|
||||
import { CLIPROXY_FALLBACK_VERSION } from '../platform-detector';
|
||||
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
|
||||
import { CLIProxyBackend } from '../types';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
|
||||
/** Binary status result */
|
||||
export interface BinaryStatusResult {
|
||||
@@ -28,6 +30,7 @@ export interface BinaryStatusResult {
|
||||
pinnedVersion: string | null;
|
||||
binaryPath: string;
|
||||
fallbackVersion: string;
|
||||
backend: CLIProxyBackend;
|
||||
}
|
||||
|
||||
/** Install result */
|
||||
@@ -48,15 +51,19 @@ export interface LatestVersionResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current binary status
|
||||
* Get current binary status for a specific backend
|
||||
*/
|
||||
export function getBinaryStatus(): BinaryStatusResult {
|
||||
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
|
||||
const effectiveBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
const backendConfig = BACKEND_CONFIG[effectiveBackend];
|
||||
return {
|
||||
installed: isCLIProxyInstalled(),
|
||||
currentVersion: getInstalledCliproxyVersion(),
|
||||
pinnedVersion: getPinnedVersion(),
|
||||
binaryPath: getCLIProxyPath(),
|
||||
fallbackVersion: CLIPROXY_FALLBACK_VERSION,
|
||||
fallbackVersion: backendConfig.fallbackVersion,
|
||||
backend: effectiveBackend,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CLIProxyProfileName } from '../../auth/profile-detector';
|
||||
import { CLIProxyProvider } from '../types';
|
||||
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS } from '../types';
|
||||
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { DEFAULT_BACKEND } from '../platform-detector';
|
||||
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||
import { deleteConfigForPort } from '../config-generator';
|
||||
import { deleteSessionLockForPort } from '../session-tracker';
|
||||
@@ -64,6 +66,22 @@ export function validateProfileName(name: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate provider/backend compatibility
|
||||
* Returns error message if provider requires Plus backend but original is configured
|
||||
*/
|
||||
export function validateProviderBackend(provider: CLIProxyProfileName): string | null {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
const backend: CLIProxyBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
|
||||
// Normalize provider to lowercase for case-insensitive comparison
|
||||
const normalizedProvider = provider.toLowerCase() as CLIProxyProvider;
|
||||
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(normalizedProvider)) {
|
||||
return `${provider} requires CLIProxyAPIPlus. Set \`cliproxy.backend: plus\` in config.yaml or use --backend=plus`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CLIProxy variant profile exists
|
||||
*/
|
||||
@@ -88,6 +106,12 @@ export function createVariant(
|
||||
account?: string
|
||||
): VariantOperationResult {
|
||||
try {
|
||||
// Validate provider/backend compatibility (block kiro/ghcp on original backend)
|
||||
const backendError = validateProviderBackend(provider);
|
||||
if (backendError) {
|
||||
return { success: false, error: backendError };
|
||||
}
|
||||
|
||||
// Allocate unique port for this variant
|
||||
const port = getNextAvailablePort();
|
||||
|
||||
@@ -188,6 +212,14 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
||||
// Update config entry if provider or account changed
|
||||
if (updates.provider !== undefined || updates.account !== undefined) {
|
||||
const newProvider = updates.provider ?? existing.provider;
|
||||
|
||||
// Validate provider/backend compatibility on provider change
|
||||
if (updates.provider !== undefined) {
|
||||
const backendError = validateProviderBackend(updates.provider);
|
||||
if (backendError) {
|
||||
return { success: false, error: backendError };
|
||||
}
|
||||
}
|
||||
const newAccount = updates.account !== undefined ? updates.account : existing.account;
|
||||
|
||||
if (isUnifiedMode()) {
|
||||
|
||||
@@ -119,6 +119,18 @@ export interface DownloadResult {
|
||||
*/
|
||||
export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
|
||||
/**
|
||||
* CLIProxy backend selection
|
||||
* - original: CLIProxyAPI (no Kiro/ghcp support)
|
||||
* - plus: CLIProxyAPIPlus (Kiro/ghcp support, default)
|
||||
*/
|
||||
export type CLIProxyBackend = 'original' | 'plus';
|
||||
|
||||
/**
|
||||
* Providers that require CLIProxyAPIPlus backend
|
||||
*/
|
||||
export const PLUS_ONLY_PROVIDERS: CLIProxyProvider[] = ['kiro', 'ghcp'];
|
||||
|
||||
/**
|
||||
* CLIProxy config.yaml structure (minimal)
|
||||
*/
|
||||
|
||||
@@ -29,11 +29,11 @@ import {
|
||||
} from '../cliproxy/account-manager';
|
||||
import { fetchAllProviderQuotas } from '../cliproxy/quota-fetcher';
|
||||
import { isOnCooldown } from '../cliproxy/quota-manager';
|
||||
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
||||
import { DEFAULT_BACKEND, getFallbackVersion, BACKEND_CONFIG } from '../cliproxy/platform-detector';
|
||||
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
||||
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog';
|
||||
import { CLIProxyProvider } from '../cliproxy/types';
|
||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
||||
import { CLIProxyProvider, CLIProxyBackend } from '../cliproxy/types';
|
||||
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||
import {
|
||||
initUI,
|
||||
header,
|
||||
@@ -68,6 +68,49 @@ import {
|
||||
// ARGUMENT PARSING
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parse --backend flag from args
|
||||
* Returns the backend value and remaining args without --backend flag
|
||||
*/
|
||||
function parseBackendArg(args: string[]): {
|
||||
backend: CLIProxyBackend | undefined;
|
||||
remainingArgs: string[];
|
||||
} {
|
||||
const backendIdx = args.indexOf('--backend');
|
||||
if (backendIdx === -1) {
|
||||
// Also check for --backend=value format
|
||||
const backendEqualsIdx = args.findIndex((a) => a.startsWith('--backend='));
|
||||
if (backendEqualsIdx !== -1) {
|
||||
const value = args[backendEqualsIdx].split('=')[1] as CLIProxyBackend;
|
||||
if (value !== 'original' && value !== 'plus') {
|
||||
warn(`Invalid backend '${value}'. Valid options: original, plus`);
|
||||
return { backend: undefined, remainingArgs: args };
|
||||
}
|
||||
const remainingArgs = [...args];
|
||||
remainingArgs.splice(backendEqualsIdx, 1);
|
||||
return { backend: value, remainingArgs };
|
||||
}
|
||||
return { backend: undefined, remainingArgs: args };
|
||||
}
|
||||
const value = args[backendIdx + 1];
|
||||
if (value !== 'original' && value !== 'plus') {
|
||||
warn(`Invalid backend '${value}'. Valid options: original, plus`);
|
||||
return { backend: undefined, remainingArgs: args };
|
||||
}
|
||||
const remainingArgs = [...args];
|
||||
remainingArgs.splice(backendIdx, 2);
|
||||
return { backend: value, remainingArgs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective backend (CLI flag > config.yaml > default)
|
||||
*/
|
||||
function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend {
|
||||
if (cliBackend) return cliBackend;
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
}
|
||||
|
||||
interface CliproxyProfileArgs {
|
||||
name?: string;
|
||||
provider?: CLIProxyProfileName;
|
||||
@@ -431,14 +474,18 @@ async function handleProxyStatus(): Promise<void> {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async function showStatus(verbose: boolean): Promise<void> {
|
||||
async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise<void> {
|
||||
await initUI();
|
||||
const status = getBinaryStatus();
|
||||
const status = getBinaryStatus(backend);
|
||||
|
||||
console.log('');
|
||||
console.log(color('CLIProxy Plus Status', 'primary'));
|
||||
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy (Original)';
|
||||
console.log(color(`${backendLabel} Status`, 'primary'));
|
||||
console.log('');
|
||||
|
||||
console.log(
|
||||
` Backend: ${color(backend, 'info')}${backend === DEFAULT_BACKEND ? dim(' (default)') : ''}`
|
||||
);
|
||||
if (status.installed) {
|
||||
console.log(` Installed: ${color('Yes', 'success')}`);
|
||||
const versionLabel = status.pinnedVersion
|
||||
@@ -576,7 +623,13 @@ async function showHelp(): Promise<void> {
|
||||
['--update', 'Unpin and update to latest version'],
|
||||
],
|
||||
],
|
||||
['Options:', [['--verbose, -v', 'Show detailed quota fetch diagnostics']]],
|
||||
[
|
||||
'Options:',
|
||||
[
|
||||
['--backend <type>', 'Use specific backend: original | plus (default: from config)'],
|
||||
['--verbose, -v', 'Show detailed quota fetch diagnostics'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
for (const [title, cmds] of sections) {
|
||||
@@ -591,9 +644,9 @@ async function showHelp(): Promise<void> {
|
||||
console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.'));
|
||||
console.log('');
|
||||
console.log(subheader('Notes:'));
|
||||
console.log(` Default fallback version: ${color(CLIPROXY_FALLBACK_VERSION, 'info')}`);
|
||||
console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`);
|
||||
console.log(
|
||||
` Releases: ${color('https://github.com/router-for-me/CLIProxyAPIPlus/releases', 'path')}`
|
||||
` Releases: ${color(`https://github.com/${BACKEND_CONFIG[DEFAULT_BACKEND].repo}/releases`, 'path')}`
|
||||
);
|
||||
console.log('');
|
||||
}
|
||||
@@ -909,16 +962,20 @@ async function handleQuotaStatus(verbose = false): Promise<void> {
|
||||
// ============================================================================
|
||||
|
||||
export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||
const verbose = args.includes('--verbose') || args.includes('-v');
|
||||
const command = args[0];
|
||||
// Parse --backend flag first (before other processing)
|
||||
const { backend: cliBackend, remainingArgs } = parseBackendArg(args);
|
||||
const effectiveBackend = getEffectiveBackend(cliBackend);
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
const verbose = remainingArgs.includes('--verbose') || remainingArgs.includes('-v');
|
||||
const command = remainingArgs[0];
|
||||
|
||||
if (remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
|
||||
await showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'create') {
|
||||
await handleCreate(args.slice(1));
|
||||
await handleCreate(remainingArgs.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -928,7 +985,7 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
if (command === 'remove' || command === 'delete' || command === 'rm') {
|
||||
await handleRemove(args.slice(1));
|
||||
await handleRemove(remainingArgs.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -949,17 +1006,17 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||
|
||||
// Quota management commands
|
||||
if (command === 'default') {
|
||||
await handleSetDefault(args.slice(1));
|
||||
await handleSetDefault(remainingArgs.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'pause') {
|
||||
await handlePauseAccount(args.slice(1));
|
||||
await handlePauseAccount(remainingArgs.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'resume') {
|
||||
await handleResumeAccount(args.slice(1));
|
||||
await handleResumeAccount(remainingArgs.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -968,9 +1025,9 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const installIdx = args.indexOf('--install');
|
||||
const installIdx = remainingArgs.indexOf('--install');
|
||||
if (installIdx !== -1) {
|
||||
let version = args[installIdx + 1];
|
||||
let version = remainingArgs[installIdx + 1];
|
||||
if (!version || version.startsWith('-')) {
|
||||
console.error(fail('Missing version argument for --install'));
|
||||
console.error(' Usage: ccs cliproxy --install <version>');
|
||||
@@ -983,10 +1040,10 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.includes('--latest') || args.includes('--update')) {
|
||||
if (remainingArgs.includes('--latest') || remainingArgs.includes('--update')) {
|
||||
await handleInstallLatest(verbose);
|
||||
return;
|
||||
}
|
||||
|
||||
await showStatus(verbose);
|
||||
await showStatus(verbose, effectiveBackend);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,8 @@ export interface TokenRefreshSettings {
|
||||
* CLIProxy configuration section.
|
||||
*/
|
||||
export interface CLIProxyConfig {
|
||||
/** Backend selection: 'original' or 'plus' (default: 'plus') */
|
||||
backend?: 'original' | 'plus';
|
||||
/** Nickname to email mapping for OAuth accounts */
|
||||
oauth_accounts: OAuthAccounts;
|
||||
/** Built-in providers (read-only, for reference) */
|
||||
@@ -530,6 +532,7 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
||||
accounts: {},
|
||||
profiles: {},
|
||||
cliproxy: {
|
||||
backend: 'plus',
|
||||
oauth_accounts: {},
|
||||
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
|
||||
variants: {},
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { testConnection } from '../../cliproxy/remote-proxy-client';
|
||||
import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service';
|
||||
import {
|
||||
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
||||
CliproxyServerConfig,
|
||||
@@ -65,6 +66,67 @@ router.put('/', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy-server/backend - Get CLIProxy backend setting
|
||||
* @returns {{ backend: 'original' | 'plus' }} Current backend configuration
|
||||
*/
|
||||
router.get('/backend', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const config = await loadOrCreateUnifiedConfig();
|
||||
res.json({ backend: config.cliproxy?.backend ?? 'plus' });
|
||||
} catch (error) {
|
||||
console.error('[cliproxy-server-routes] Failed to load backend config:', error);
|
||||
res.status(500).json({ error: 'Failed to load backend config' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/cliproxy-server/backend - Update CLIProxy backend setting
|
||||
* @param {Object} req.body - Request body
|
||||
* @param {'original' | 'plus'} req.body.backend - Backend to switch to
|
||||
* @param {boolean} [req.body.force=false] - Force change even if proxy is running
|
||||
* @returns {{ backend: 'original' | 'plus' }} Updated backend configuration
|
||||
* @throws {400} Invalid backend value
|
||||
* @throws {409} Proxy is running (unless force=true)
|
||||
*/
|
||||
router.put('/backend', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { backend, force } = req.body;
|
||||
if (backend !== 'original' && backend !== 'plus') {
|
||||
res.status(400).json({ error: 'Invalid backend. Must be "original" or "plus"' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if proxy is running - warn about restart requirement
|
||||
const config = await loadOrCreateUnifiedConfig();
|
||||
const currentBackend = config.cliproxy?.backend ?? 'plus';
|
||||
if (currentBackend !== backend && isProxyRunning() && !force) {
|
||||
res.status(409).json({
|
||||
error: 'Proxy is running. Stop proxy first or use force=true to change backend.',
|
||||
proxyRunning: true,
|
||||
currentBackend,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!config.cliproxy) {
|
||||
config.cliproxy = {
|
||||
backend,
|
||||
oauth_accounts: {},
|
||||
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
|
||||
variants: {},
|
||||
};
|
||||
} else {
|
||||
config.cliproxy.backend = backend;
|
||||
}
|
||||
|
||||
await saveUnifiedConfig(config);
|
||||
res.json({ backend });
|
||||
} catch (error) {
|
||||
console.error('[cliproxy-server-routes] Failed to save backend config:', error);
|
||||
res.status(500).json({ error: 'Failed to save backend config' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/cliproxy-server/test - Test remote proxy connection
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Tests for CLIProxy Backend Selection
|
||||
* Verifies backend selection feature for original vs plus CLIProxyAPI variants
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
describe('Backend Selection', () => {
|
||||
const platformDetector = require('../../../dist/cliproxy/platform-detector');
|
||||
const types = require('../../../dist/cliproxy/types');
|
||||
|
||||
describe('BACKEND_CONFIG', () => {
|
||||
it('has correct configuration for original backend', () => {
|
||||
const config = platformDetector.BACKEND_CONFIG.original;
|
||||
assert.strictEqual(config.repo, 'router-for-me/CLIProxyAPI');
|
||||
assert.strictEqual(config.binaryPrefix, 'CLIProxyAPI');
|
||||
assert.strictEqual(config.executable, 'cli-proxy-api');
|
||||
assert(config.fallbackVersion.match(/^\d+\.\d+\.\d+$/), 'original version has no suffix');
|
||||
});
|
||||
|
||||
it('has correct configuration for plus backend', () => {
|
||||
const config = platformDetector.BACKEND_CONFIG.plus;
|
||||
assert.strictEqual(config.repo, 'router-for-me/CLIProxyAPIPlus');
|
||||
assert.strictEqual(config.binaryPrefix, 'CLIProxyAPIPlus');
|
||||
assert.strictEqual(config.executable, 'cli-proxy-api-plus');
|
||||
assert(config.fallbackVersion.match(/^\d+\.\d+\.\d+-\d+$/), 'plus version has -0 suffix');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_BACKEND', () => {
|
||||
it('defaults to plus backend for backward compatibility', () => {
|
||||
assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'plus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectPlatform', () => {
|
||||
it('generates correct binary name for original backend', () => {
|
||||
const info = platformDetector.detectPlatform('6.6.51', 'original');
|
||||
assert(info.binaryName.startsWith('CLIProxyAPI_6.6.51_'));
|
||||
assert(!info.binaryName.includes('CLIProxyAPIPlus'));
|
||||
});
|
||||
|
||||
it('generates correct binary name for plus backend', () => {
|
||||
const info = platformDetector.detectPlatform('6.6.51-0', 'plus');
|
||||
assert(info.binaryName.startsWith('CLIProxyAPIPlus_6.6.51-0_'));
|
||||
});
|
||||
|
||||
it('uses plus backend by default', () => {
|
||||
const info = platformDetector.detectPlatform();
|
||||
assert(info.binaryName.includes('CLIProxyAPIPlus'));
|
||||
});
|
||||
|
||||
it('uses fallback version when version not specified', () => {
|
||||
const infoOriginal = platformDetector.detectPlatform(undefined, 'original');
|
||||
const fallbackOriginal = platformDetector.BACKEND_CONFIG.original.fallbackVersion;
|
||||
assert(infoOriginal.binaryName.includes(fallbackOriginal));
|
||||
|
||||
const infoPlus = platformDetector.detectPlatform(undefined, 'plus');
|
||||
const fallbackPlus = platformDetector.BACKEND_CONFIG.plus.fallbackVersion;
|
||||
assert(infoPlus.binaryName.includes(fallbackPlus));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExecutableName', () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
it('returns correct name for original backend', () => {
|
||||
const name = platformDetector.getExecutableName('original');
|
||||
const expected = isWindows ? 'cli-proxy-api.exe' : 'cli-proxy-api';
|
||||
assert.strictEqual(name, expected);
|
||||
});
|
||||
|
||||
it('returns correct name for plus backend', () => {
|
||||
const name = platformDetector.getExecutableName('plus');
|
||||
const expected = isWindows ? 'cli-proxy-api-plus.exe' : 'cli-proxy-api-plus';
|
||||
assert.strictEqual(name, expected);
|
||||
});
|
||||
|
||||
it('defaults to plus backend', () => {
|
||||
const name = platformDetector.getExecutableName();
|
||||
assert(name.includes('cli-proxy-api-plus'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadUrl', () => {
|
||||
it('uses correct repo for original backend', () => {
|
||||
const url = platformDetector.getDownloadUrl('6.6.51', 'original');
|
||||
assert(url.includes('router-for-me/CLIProxyAPI/releases'));
|
||||
assert(!url.includes('CLIProxyAPIPlus'));
|
||||
});
|
||||
|
||||
it('uses correct repo for plus backend', () => {
|
||||
const url = platformDetector.getDownloadUrl('6.6.51-0', 'plus');
|
||||
assert(url.includes('router-for-me/CLIProxyAPIPlus/releases'));
|
||||
});
|
||||
|
||||
it('defaults to plus backend', () => {
|
||||
const url = platformDetector.getDownloadUrl();
|
||||
assert(url.includes('CLIProxyAPIPlus'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFallbackVersion', () => {
|
||||
it('returns correct version for original backend', () => {
|
||||
const version = platformDetector.getFallbackVersion('original');
|
||||
assert.strictEqual(version, platformDetector.BACKEND_CONFIG.original.fallbackVersion);
|
||||
});
|
||||
|
||||
it('returns correct version for plus backend', () => {
|
||||
const version = platformDetector.getFallbackVersion('plus');
|
||||
assert.strictEqual(version, platformDetector.BACKEND_CONFIG.plus.fallbackVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PLUS_ONLY_PROVIDERS', () => {
|
||||
it('includes kiro as plus-only provider', () => {
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('kiro'));
|
||||
});
|
||||
|
||||
it('includes ghcp as plus-only provider', () => {
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('ghcp'));
|
||||
});
|
||||
|
||||
it('does not include gemini as plus-only provider', () => {
|
||||
assert(!types.PLUS_ONLY_PROVIDERS.includes('gemini'));
|
||||
});
|
||||
|
||||
it('does not include agy as plus-only provider', () => {
|
||||
assert(!types.PLUS_ONLY_PROVIDERS.includes('agy'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -495,6 +495,14 @@ export const api = {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
/** Get backend setting */
|
||||
getBackend: () => request<{ backend: 'original' | 'plus' }>('/cliproxy-server/backend'),
|
||||
/** Update backend setting */
|
||||
updateBackend: (backend: 'original' | 'plus', force = false) =>
|
||||
request<{ backend: 'original' | 'plus' }>('/cliproxy-server/backend', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ backend, force }),
|
||||
}),
|
||||
/** Test remote proxy connection */
|
||||
test: (params: {
|
||||
host: string;
|
||||
|
||||
@@ -3,19 +3,33 @@
|
||||
* Settings section for CLIProxyAPI configuration (local/remote)
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { RefreshCw, CheckCircle2, AlertCircle, Laptop, Cloud, Bug } from 'lucide-react';
|
||||
import {
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Laptop,
|
||||
Cloud,
|
||||
Bug,
|
||||
Box,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useProxyConfig, useRawConfig } from '../../hooks';
|
||||
import { LocalProxyCard } from './local-proxy-card';
|
||||
import { RemoteProxyCard } from './remote-proxy-card';
|
||||
import { api } from '@/lib/api-client';
|
||||
|
||||
/** LocalStorage key for debug mode preference */
|
||||
const DEBUG_MODE_KEY = 'ccs_debug_mode';
|
||||
|
||||
/** Providers only available on CLIProxyAPIPlus */
|
||||
const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp'];
|
||||
|
||||
export default function ProxySection() {
|
||||
const {
|
||||
config,
|
||||
@@ -60,6 +74,52 @@ export default function ProxySection() {
|
||||
}
|
||||
};
|
||||
|
||||
// Backend state (loaded from API)
|
||||
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
|
||||
const [backendSaving, setBackendSaving] = useState(false);
|
||||
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
|
||||
|
||||
// Fetch backend setting
|
||||
const fetchBackend = useCallback(async () => {
|
||||
try {
|
||||
const result = await api.cliproxyServer.getBackend();
|
||||
setBackend(result.backend);
|
||||
} catch (err) {
|
||||
console.error('[Proxy] Failed to fetch backend:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check for Kiro/ghcp variants
|
||||
const checkPlusOnlyVariants = useCallback(async () => {
|
||||
try {
|
||||
const result = await api.cliproxy.list();
|
||||
const hasIncompatible = result.variants.some((v) => PLUS_ONLY_PROVIDERS.includes(v.provider));
|
||||
setHasKiroGhcpVariants(hasIncompatible);
|
||||
} catch (err) {
|
||||
console.error('[Proxy] Failed to check variants:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save backend setting
|
||||
const handleBackendChange = async (value: 'original' | 'plus') => {
|
||||
const previousValue = backend;
|
||||
setBackend(value);
|
||||
setBackendSaving(true);
|
||||
try {
|
||||
await api.cliproxyServer.updateBackend(value);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to save backend';
|
||||
// Check if error is due to proxy running (409 conflict)
|
||||
if (errorMessage.includes('Proxy is running')) {
|
||||
toast.error('Stop the proxy first to change backend');
|
||||
}
|
||||
console.error('[Proxy] Failed to save backend:', err);
|
||||
setBackend(previousValue);
|
||||
} finally {
|
||||
setBackendSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Log when debug mode changes (sanitize sensitive fields)
|
||||
useEffect(() => {
|
||||
if (debugMode && config) {
|
||||
@@ -80,7 +140,9 @@ export default function ProxySection() {
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchRawConfig();
|
||||
}, [fetchConfig, fetchRawConfig]);
|
||||
fetchBackend();
|
||||
checkPlusOnlyVariants();
|
||||
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
|
||||
|
||||
if (loading || !config) {
|
||||
return (
|
||||
@@ -242,6 +304,64 @@ export default function ProxySection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backend Selection - Card based selection */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-base font-medium flex items-center gap-2">
|
||||
<Box className="w-4 h-4" />
|
||||
Backend Binary
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Plus Backend Card */}
|
||||
<button
|
||||
onClick={() => handleBackendChange('plus')}
|
||||
disabled={backendSaving}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
backend === 'plus'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-muted-foreground/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-medium">CLIProxyAPIPlus</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400">
|
||||
Default
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Full provider support including Kiro and GitHub Copilot
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* Original Backend Card */}
|
||||
<button
|
||||
onClick={() => handleBackendChange('original')}
|
||||
disabled={backendSaving}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
backend === 'original'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-muted-foreground/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-medium">CLIProxyAPI</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Original binary (Gemini, Codex, Antigravity only)
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
{/* Warning when original backend selected with Kiro/ghcp variants */}
|
||||
{backend === 'original' && hasKiroGhcpVariants && (
|
||||
<Alert variant="destructive" className="py-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Existing Kiro/Copilot variants will not work with CLIProxyAPI. Switch to
|
||||
CLIProxyAPIPlus or remove those variants.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remote Settings - Show when remote mode is enabled */}
|
||||
{isRemoteMode && (
|
||||
<RemoteProxyCard
|
||||
@@ -357,6 +477,8 @@ export default function ProxySection() {
|
||||
onClick={() => {
|
||||
fetchConfig();
|
||||
fetchRawConfig();
|
||||
fetchBackend();
|
||||
checkPlusOnlyVariants();
|
||||
}}
|
||||
disabled={loading || saving}
|
||||
className="w-full"
|
||||
|
||||
Reference in New Issue
Block a user