hotfix: close cliproxy plus fallback gaps

This commit is contained in:
Tam Nhu Tran
2026-04-22 08:52:17 -04:00
parent b8b48a99ba
commit dd90ea7e2f
15 changed files with 235 additions and 104 deletions
+59 -22
View File
@@ -31,12 +31,47 @@ import {
import type { CLIProxyBackend } from './types';
export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062';
/**
* Track whether we've already warned the user about the Plus fallback this
* process lifetime. Prevents spamming the warning on every command.
*/
let plusFallbackWarned = false;
function emitPlusFallbackWarning(): void {
if (plusFallbackWarned) return;
plusFallbackWarned = true;
process.stderr.write(
`${warn(
'CLIProxyAPIPlus upstream repo is currently unavailable; local CLIProxy is falling back to ' +
'`backend: original`. Run `ccs config` to update your saved config. ' +
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
)}\n`
);
}
export function getPlusBackendUnavailableMessage(provider?: string): string {
const prefix = provider
? `${provider} requires CLIProxyAPIPlus,`
: 'CLIProxyAPIPlus upstream repo is currently unavailable,';
return (
`${prefix} but local CLIProxy currently supports only \`backend: original\`. ` +
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
);
}
export function resolveLocalBackend(
backend: CLIProxyBackend = DEFAULT_BACKEND,
options: { warnOnFallback?: boolean } = {}
): CLIProxyBackend {
if (backend !== 'plus') return backend;
if (options.warnOnFallback) {
emitPlusFallbackWarning();
}
return 'original';
}
/**
* Get backend from config, with runtime fallback to 'original' when the user
* still has `backend: plus` saved.
@@ -47,7 +82,7 @@ let plusFallbackWarned = false;
* runtime and warn once. This keeps existing installations working without a
* reconfig step, while CCS self-maintains its own Plus fork (future work).
*/
function getConfiguredBackend(): CLIProxyBackend {
export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend {
let configured: CLIProxyBackend;
try {
const config = loadOrCreateUnifiedConfig();
@@ -56,19 +91,7 @@ function getConfiguredBackend(): CLIProxyBackend {
return DEFAULT_BACKEND;
}
if (configured === 'plus') {
if (!plusFallbackWarned) {
plusFallbackWarned = true;
warn(
'CLIProxyAPIPlus upstream repo is currently unavailable; falling back to ' +
'`backend: original`. Run `ccs config` to update your saved config. ' +
'Tracking: https://github.com/kaitranntt/ccs/issues/1062'
);
}
return 'original';
}
return configured;
return resolveLocalBackend(configured, options);
}
/**
@@ -104,7 +127,9 @@ export class BinaryManager {
private backend: CLIProxyBackend;
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
this.backend = backend ?? getConfiguredBackend();
this.backend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
const defaultConfig = createDefaultConfig(this.backend);
this.config = { ...defaultConfig, ...config };
}
@@ -155,7 +180,7 @@ export async function ensureCLIProxyBinary(
verbose = false,
options: EnsureCLIProxyBinaryOptions = {}
): Promise<string> {
const backend = getConfiguredBackend();
const backend = getConfiguredBackend({ warnOnFallback: true });
// Migrate old shared pin to backend-specific location (one-time migration)
migrateVersionPin(backend);
@@ -186,19 +211,25 @@ export async function ensureCLIProxyBinary(
/** Check if CLIProxyAPI binary is installed */
export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean {
const effectiveBackend = backend ?? getConfiguredBackend();
const effectiveBackend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
return new BinaryManager({}, effectiveBackend).isBinaryInstalled();
}
/** Get CLIProxyAPI binary path (may not exist) */
export function getCLIProxyPath(backend?: CLIProxyBackend): string {
const effectiveBackend = backend ?? getConfiguredBackend();
const effectiveBackend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
return new BinaryManager({}, effectiveBackend).getBinaryPath();
}
/** Get installed CLIProxyAPI version from .version file */
export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
const effectiveBackend = backend ?? getConfiguredBackend();
const effectiveBackend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
return readInstalledVersion(
getBackendBinDir(effectiveBackend),
BACKEND_CONFIG[effectiveBackend].fallbackVersion
@@ -224,7 +255,9 @@ export async function installCliproxyVersion(
backend?: CLIProxyBackend,
deps: InstallCliproxyVersionDeps = {}
): Promise<void> {
const effectiveBackend = backend ?? getConfiguredBackend();
const effectiveBackend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
const manager =
deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ??
new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
@@ -265,7 +298,9 @@ export async function installCliproxyVersion(
/** Fetch the latest CLIProxyAPI version from GitHub API */
export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> {
const effectiveBackend = backend ?? getConfiguredBackend();
const effectiveBackend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
return result.latestVersion;
}
@@ -290,7 +325,9 @@ export interface CliproxyUpdateCheckResult {
export async function checkCliproxyUpdate(
backend?: CLIProxyBackend
): Promise<CliproxyUpdateCheckResult> {
const effectiveBackend = backend ?? getConfiguredBackend();
const effectiveBackend = backend
? resolveLocalBackend(backend, { warnOnFallback: true })
: getConfiguredBackend({ warnOnFallback: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
// Import isNewerVersion for stability check
+21 -19
View File
@@ -18,7 +18,11 @@ import { ProgressIndicator } from '../../utils/progress-indicator';
import { ok, fail, info, warn } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import { escapeShellArg, getWindowsEscapedCommandShell } from '../../utils/shell-executor';
import { ensureCLIProxyBinary } from '../binary-manager';
import {
ensureCLIProxyBinary,
getConfiguredBackend,
getPlusBackendUnavailableMessage,
} from '../binary-manager';
import {
generateConfig,
getProviderConfig,
@@ -30,7 +34,6 @@ import {
import { checkRemoteProxy } from '../remote-proxy-client';
import { isAuthenticated } from '../auth-handler';
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types';
import { DEFAULT_BACKEND } from '../platform-detector';
import { configureProviderModel, getCurrentModel } from '../model-config';
import { reconcileCodexModelForActivePlan } from '../codex-plan-compatibility';
import { resolveProxyConfig, PROXY_CLI_FLAGS } from '../proxy-config-resolver';
@@ -211,23 +214,8 @@ export async function execClaudeWithCLIProxy(
// 0. Resolve proxy configuration (CLI > ENV > config.yaml > defaults)
const unifiedConfig = loadOrCreateUnifiedConfig();
// 0a. Runtime backend/provider validation
const backend: CLIProxyBackend = unifiedConfig.cliproxy?.backend ?? DEFAULT_BACKEND;
// Collect all providers to validate (default + composite tiers)
const allProviders = [provider, ...compositeProviders];
for (const p of allProviders) {
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
console.error('');
console.error(fail(`${p} 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 ' + p + ' --backend=plus');
console.error('');
throw new Error(`Provider ${p} requires Plus backend`);
}
}
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
@@ -314,6 +302,7 @@ export async function execClaudeWithCLIProxy(
// Check remote proxy if configured
let useRemoteProxy = false;
let localBackend: CLIProxyBackend = 'original';
if (proxyConfig.mode === 'remote' && proxyConfig.host) {
const status = await checkRemoteProxy({
host: proxyConfig.host,
@@ -361,6 +350,19 @@ export async function execClaudeWithCLIProxy(
}
}
if (!useRemoteProxy) {
localBackend = getConfiguredBackend({ warnOnFallback: true });
for (const p of allProviders) {
if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
console.error('');
console.error(fail(getPlusBackendUnavailableMessage(p)));
console.error('');
throw new Error(`Provider ${p} is temporarily unavailable on local CLIProxy`);
}
}
}
// Variables for local proxy mode
let binaryPath: string | undefined;
let sessionId: string | undefined;
@@ -1000,13 +1002,13 @@ export async function execClaudeWithCLIProxy(
cfg.port,
cfg.timeout,
cfg.pollInterval,
backend,
localBackend,
configPath
);
// Register session
if (proxy.pid) {
sessionId = registerProxySession(cfg.port, proxy.pid, backend, verbose);
sessionId = registerProxySession(cfg.port, proxy.pid, localBackend, verbose);
}
}
}
+29 -14
View File
@@ -18,6 +18,7 @@ import {
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
resolveLocalBackend,
} from '../binary-manager';
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
import { CLIProxyBackend } from '../types';
@@ -54,8 +55,10 @@ export interface LatestVersionResult {
* Get current binary status for a specific backend
*/
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
const backendConfig = BACKEND_CONFIG[effectiveBackend];
return {
installed: isCLIProxyInstalled(effectiveBackend),
@@ -71,8 +74,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
* Check for latest version
*/
export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> {
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
try {
// Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo)
@@ -119,8 +124,10 @@ export async function installVersion(
};
}
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
try {
await installCliproxyVersion(version, verbose, effectiveBackend);
@@ -147,8 +154,10 @@ export async function installLatest(
verbose = false,
backend?: CLIProxyBackend
): Promise<InstallResult> {
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
try {
const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend);
@@ -184,8 +193,10 @@ export async function installLatest(
* Check if a version is pinned
*/
export function isPinned(backend?: CLIProxyBackend): boolean {
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
return isVersionPinned(effectiveBackend);
}
@@ -193,8 +204,10 @@ export function isPinned(backend?: CLIProxyBackend): boolean {
* Get pinned version if any
*/
export function getPinned(backend?: CLIProxyBackend): string | null {
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
return getPinnedVersion(effectiveBackend);
}
@@ -202,7 +215,9 @@ export function getPinned(backend?: CLIProxyBackend): string | null {
* Clear version pin
*/
export function clearPin(backend?: CLIProxyBackend): void {
const effectiveBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const effectiveBackend = resolveLocalBackend(
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND,
{ warnOnFallback: true }
);
clearPinnedVersion(effectiveBackend);
}
+6 -8
View File
@@ -8,12 +8,10 @@
import * as os from 'os';
import * as path from 'path';
import { CLIProxyProfileName } from '../../auth/profile-detector';
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS } from '../types';
import { CLIProxyProvider, PLUS_ONLY_PROVIDERS } from '../types';
import { CompositeTierConfig, CompositeVariantConfig } from '../../config/unified-config-types';
import type { TargetType } from '../../targets/target-adapter';
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 { hasActiveSessions, deleteSessionLockForPort } from '../session-tracker';
@@ -44,6 +42,7 @@ import {
removeVariantFromLegacyConfig,
getNextAvailablePort,
} from './variant-config-adapter';
import { getConfiguredBackend, getPlusBackendUnavailableMessage } from '../binary-manager';
// Re-export VariantConfig from adapter
export type { VariantConfig } from './variant-config-adapter';
@@ -80,16 +79,15 @@ export function validateProfileName(name: string): string | null {
/**
* Validate provider/backend compatibility
* Returns error message if provider requires Plus backend but original is configured
* Returns error message if a provider requires Plus while local CLIProxy is
* running with the fallbacked original backend.
*/
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;
const backend = getConfiguredBackend();
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(normalizedProvider)) {
return `${provider} requires CLIProxyAPIPlus. Set \`cliproxy.backend: plus\` in config.yaml or use --backend=plus`;
return getPlusBackendUnavailableMessage(provider);
}
return null;
}
+5 -5
View File
@@ -6,14 +6,13 @@
*/
import { CLIProxyBackend } from '../../cliproxy/types';
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
import { getConfiguredBackend, resolveLocalBackend } from '../../cliproxy/binary-manager';
import {
type QuotaSupportedProvider,
QUOTA_PROVIDER_HELP_TEXT,
mapExternalProviderName,
isQuotaSupportedProvider,
} from '../../cliproxy/provider-capabilities';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { handleSync } from '../cliproxy-sync-handler';
import { extractOption, hasAnyFlag } from '../arg-extractor';
@@ -74,9 +73,10 @@ function parseBackendArg(args: string[]): {
* 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;
if (cliBackend) {
return resolveLocalBackend(cliBackend, { warnOnFallback: true });
}
return getConfiguredBackend({ warnOnFallback: true });
}
/**
+1 -1
View File
@@ -47,7 +47,7 @@ export async function showStatus(verbose: boolean, backend: CLIProxyBackend): Pr
console.log(` ${dim('Run "ccs gemini" or any provider to auto-install')}`);
}
const latestCheck = await checkLatestVersion();
const latestCheck = await checkLatestVersion(backend);
if (latestCheck.success && latestCheck.latestVersion) {
console.log('');
if (latestCheck.updateAvailable) {
+1 -1
View File
@@ -410,7 +410,7 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
backend:
partial.cliproxy?.backend === 'original' || partial.cliproxy?.backend === 'plus'
? partial.cliproxy.backend
: undefined, // Invalid values become undefined (defaults to 'plus' at runtime)
: undefined, // Invalid values become undefined (defaults to 'original' at runtime)
// Auto-sync - default to true
auto_sync: partial.cliproxy?.auto_sync ?? defaults.cliproxy.auto_sync ?? true,
routing: {
+1 -1
View File
@@ -211,7 +211,7 @@ export interface CLIProxyRoutingConfig {
* CLIProxy configuration section.
*/
export interface CLIProxyConfig {
/** Backend selection: 'original' or 'plus' (default: 'plus') */
/** Backend selection: 'original' or 'plus' (default: 'original') */
backend?: 'original' | 'plus';
/** Nickname to email mapping for OAuth accounts */
oauth_accounts: OAuthAccounts;
+8 -16
View File
@@ -38,7 +38,11 @@ import {
} from '../../cliproxy/config-generator';
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker';
import { ensureCliproxyService } from '../../cliproxy/service-manager';
import { checkCliproxyUpdate, getInstalledCliproxyVersion } from '../../cliproxy/binary-manager';
import {
checkCliproxyUpdate,
getConfiguredBackend,
getInstalledCliproxyVersion,
} from '../../cliproxy/binary-manager';
import {
fetchAllVersions,
isNewerVersion,
@@ -47,9 +51,7 @@ import {
import {
CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE,
DEFAULT_BACKEND,
} from '../../cliproxy/platform-detector';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/port-manager';
import {
MODEL_ENV_VAR_KEYS,
@@ -143,16 +145,6 @@ export function shouldCacheQuotaResult(result: {
return !transientPatterns.some((p) => msg.includes(p));
}
/** Get configured backend from config */
function getConfiguredBackend() {
try {
const config = loadOrCreateUnifiedConfig();
return config.cliproxy?.backend || DEFAULT_BACKEND;
} catch {
return DEFAULT_BACKEND;
}
}
function buildUpdateCheckFallback(
backend: ReturnType<typeof getConfiguredBackend>,
getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion
@@ -409,7 +401,7 @@ router.post('/proxy-stop', async (_req: Request, res: Response): Promise<void> =
*/
router.get('/update-check', async (_req: Request, res: Response): Promise<void> => {
try {
const backend = getConfiguredBackend();
const backend = getConfiguredBackend({ warnOnFallback: true });
const result = await resolveCliproxyUpdateCheckPayload(backend);
res.json(result);
@@ -1019,7 +1011,7 @@ router.get('/quota/:provider/:accountId', async (req: Request, res: Response): P
*/
router.get('/versions', async (_req: Request, res: Response): Promise<void> => {
try {
const backend = getConfiguredBackend();
const backend = getConfiguredBackend({ warnOnFallback: true });
res.json(await resolveCliproxyVersionsPayload(backend));
} catch (error) {
console.error(`[cliproxy-stats] ${(error as Error).message}`);
@@ -1073,7 +1065,7 @@ router.post('/install', async (req: Request, res: Response): Promise<void> => {
return;
}
const backend = getConfiguredBackend();
const backend = getConfiguredBackend({ warnOnFallback: true });
const installResult = await installDashboardCliproxyVersion(version, backend);
res.json({
@@ -1,4 +1,4 @@
import { installCliproxyVersion } from '../../cliproxy/binary-manager';
import { installCliproxyVersion, resolveLocalBackend } from '../../cliproxy/binary-manager';
import { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/service-manager';
import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker';
import { isCliproxyRunning } from '../../cliproxy/stats-fetcher';
@@ -52,12 +52,13 @@ export async function installDashboardCliproxyVersion(
backend: CLIProxyBackend,
deps: InstallDashboardCliproxyVersionDeps = defaultDeps
): Promise<DashboardCliproxyInstallResult> {
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true });
const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const shouldRestoreService = await wasProxyRunning(deps);
// The installer owns the stop-and-replace lifecycle, including best-effort
// shutdown for tracked and untracked proxies before swapping the binary.
await deps.installCliproxyVersion(version, true, backend);
await deps.installCliproxyVersion(version, true, effectiveBackend);
if (!shouldRestoreService) {
return {
@@ -25,6 +25,54 @@ afterEach(() => {
});
describe('installCliproxyVersion', () => {
it('degrades explicit plus backend requests to original before install flows run', async () => {
let seenBackend: string | undefined;
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-explicit-plus=${Date.now()}`
);
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus', {
createManager: (_config: unknown, backend: string) => {
seenBackend = backend;
return {
isBinaryInstalled: () => false,
deleteBinary: () => undefined,
ensureBinary: async () => '/tmp/ccs-bin/original/cliproxy',
};
},
stopProxyFn: async () => ({ stopped: false, error: 'No active CLIProxy session found' }),
waitForPortFreeFn: async () => true,
formatInfo: (message: string) => message,
formatWarn: (message: string) => message,
getInstalledVersion: () => '6.6.80',
});
expect(seenBackend).toBe('original');
});
it('returns original and emits a real warning when plus backend is resolved locally', async () => {
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-warning=${Date.now()}`
);
const writes: string[] = [];
const originalWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = ((chunk: string | Uint8Array) => {
writes.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
return true;
}) as typeof process.stderr.write;
try {
expect(binaryManager.resolveLocalBackend('plus', { warnOnFallback: true })).toBe('original');
} finally {
process.stderr.write = originalWrite;
}
expect(writes.join('')).toContain('CLIProxyAPIPlus upstream repo is currently unavailable');
expect(writes.join('')).toContain('backend: original');
});
it('attempts to stop the proxy even when there is no tracked running session', async () => {
const calls = {
stopProxy: 0,
@@ -6,7 +6,10 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { updateVariant } from '../../../src/cliproxy/services/variant-service';
import {
updateVariant,
validateProviderBackend,
} from '../../../src/cliproxy/services/variant-service';
import { loadOrCreateUnifiedConfig } from '../../../src/config/unified-config-loader';
describe('updateVariant - provider/model consistency', () => {
@@ -50,6 +53,7 @@ preferences:
telemetry: false
auto_update: true
cliproxy:
backend: plus
oauth_accounts: {}
providers:
- gemini
@@ -92,6 +96,12 @@ cliproxy:
expect(result.error).toContain('denylist');
});
it('reports plus-only providers as temporarily unavailable on local CLIProxy', () => {
const error = validateProviderBackend('ghcp');
expect(error).toContain('currently supports only `backend: original`');
expect(error).toContain('issues/1062');
});
it('updates provider and regenerates provider-specific core env in same settings file', () => {
const result = updateVariant('demo', {
provider: 'codex',
@@ -56,7 +56,7 @@ describe('installDashboardCliproxyVersion', () => {
success: true,
restarted: true,
port: 8317,
message: 'Successfully installed CLIProxy Plus v6.7.1 and restarted it on port 8317',
message: 'Successfully installed CLIProxy v6.7.1 and restarted it on port 8317',
});
expect(calls.isCliproxyRunning).toBe(0);
expect(calls.installCliproxyVersion).toBe(1);
@@ -71,7 +71,7 @@ describe('installDashboardCliproxyVersion', () => {
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: true,
restarted: false,
message: 'Successfully installed CLIProxy Plus v6.7.1',
message: 'Successfully installed CLIProxy v6.7.1',
});
expect(calls.isCliproxyRunning).toBe(1);
expect(calls.installCliproxyVersion).toBe(1);
@@ -118,8 +118,8 @@ describe('installDashboardCliproxyVersion', () => {
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: false,
restarted: false,
error: 'Installed CLIProxy Plus v6.7.1, but restart failed',
message: 'Installed CLIProxy Plus v6.7.1, but failed to restart it',
error: 'Installed CLIProxy v6.7.1, but restart failed',
message: 'Installed CLIProxy v6.7.1, but failed to restart it',
});
});
});
@@ -10,6 +10,7 @@ let createEmptyUnifiedConfig: typeof import('../../../src/config/unified-config-
let saveUnifiedConfig: typeof import('../../../src/config/unified-config-loader').saveUnifiedConfig;
let setGlobalConfigDir: typeof import('../../../src/utils/config-manager').setGlobalConfigDir;
let writeInstalledVersion: typeof import('../../../src/cliproxy/binary/version-cache').writeInstalledVersion;
let writeVersionCache: typeof import('../../../src/cliproxy/binary/version-cache').writeVersionCache;
let writeVersionListCache: typeof import('../../../src/cliproxy/binary/version-cache').writeVersionListCache;
let server: Server;
@@ -25,20 +26,21 @@ beforeAll(async () => {
({ setGlobalConfigDir } = await import('../../../src/utils/config-manager'));
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
({ writeInstalledVersion, writeVersionListCache } = await import(
({ writeInstalledVersion, writeVersionCache, writeVersionListCache } = await import(
'../../../src/cliproxy/binary/version-cache'
));
const ccsDir = path.join(tempHome, '.ccs');
const plusBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
const originalBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'original');
fs.mkdirSync(originalBinDir, { recursive: true });
setGlobalConfigDir(ccsDir);
const config = createEmptyUnifiedConfig();
config.cliproxy = { backend: 'plus' };
saveUnifiedConfig(config);
writeInstalledVersion(plusBinDir, '6.6.80');
writeInstalledVersion(originalBinDir, '6.6.80');
writeVersionCache('6.6.89', 'original');
writeVersionListCache(
{
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
@@ -46,7 +48,7 @@ beforeAll(async () => {
latest: '6.6.89',
checkedAt: Date.now(),
},
'plus'
'original'
);
({ default: cliproxyStatsRoutes } = await import(
@@ -94,6 +96,23 @@ afterAll(async () => {
});
describe('cliproxy-stats-routes install contract', () => {
it('routes saved plus configs through original backend for update checks', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/update-check`);
expect(response.status).toBe(200);
const body = (await response.json()) as {
backend: string;
backendLabel: string;
currentVersion: string;
latestVersion: string;
};
expect(body.backend).toBe('original');
expect(body.backendLabel).toBe('CLIProxy');
expect(body.currentVersion).toBe('6.6.80');
expect(body.latestVersion).toBe('6.6.89');
});
it('returns faultyRange in the versions response', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/versions`);
expect(response.status).toBe(200);
+13 -4
View File
@@ -191,7 +191,7 @@ export default function ProxySection() {
}, [isAgyConfirmPhraseValid, persistAgyAckBypass, t]);
// Backend state (loaded from API) + mutation hook for proper query invalidation
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
const [backend, setBackend] = useState<'original' | 'plus'>('original');
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
const updateBackendMutation = useUpdateBackend();
const { data: proxyStatus } = useProxyStatus();
@@ -490,9 +490,6 @@ export default function ProxySection() {
>
<div className="flex items-center gap-3 mb-2">
<span className="font-medium">{t('settingsProxy.backendPlusApi')}</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">
{t('settingsProxy.default')}
</span>
</div>
<p className="text-xs text-muted-foreground">{t('settingsProxy.plusDesc')}</p>
</button>
@@ -509,10 +506,22 @@ export default function ProxySection() {
>
<div className="flex items-center gap-3 mb-2">
<span className="font-medium">{t('settingsProxy.backendApi')}</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">
{t('settingsProxy.default')}
</span>
</div>
<p className="text-xs text-muted-foreground">{t('settingsProxy.originalDesc')}</p>
</button>
</div>
{backend === 'plus' && (
<Alert className="py-2 border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20 [&>svg]:top-2.5">
<AlertTriangle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-amber-700 dark:text-amber-400">
CLIProxyAPIPlus upstream is currently unavailable. Local CLIProxy will use the
original backend until issue #1062 is resolved.
</AlertDescription>
</Alert>
)}
{/* Warning when original backend selected with Kiro/ghcp variants */}
{backend === 'original' && hasKiroGhcpVariants && (
<Alert variant="destructive" className="py-2">