fix(cliproxy): route plus backend to maintained fork

This commit is contained in:
Tam Nhu Tran
2026-04-23 21:25:29 -04:00
parent 910202988d
commit 3d22bb07f0
17 changed files with 109 additions and 150 deletions
+2 -1
View File
@@ -118,7 +118,8 @@ config. Deep dive:
Manage OAuth-backed providers, quota visibility, and proxy-wide routing from one place. CCS now
surfaces round-robin vs fill-first natively in both CLI and dashboard flows instead of hiding that
choice inside raw upstream controls.
choice inside raw upstream controls. The original CLIProxyAPI backend remains the default; the
community-maintained CLIProxyAPIPlus fork is opt-in for plus-only providers.
Deep dive:
[CLIProxy API](https://docs.ccs.kaitran.ca/features/proxy/cliproxy-api).
+1
View File
@@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic
### Recent Fixes
- **2026-04-24**: **#1065** Local CLIProxy Plus is available again as an explicit opt-in backend through the community-maintained `kaitranntt/CLIProxyAPIPlus` fork. CCS keeps `original` as the default backend, no longer downgrades saved `backend: plus` configs to `original`, updates Plus release lookups to the maintained fork, and documents Plus as a targeted path for plus-only providers.
- **2026-04-21**: CLIProxy quota failover now quarantines exhausted Claude and Antigravity accounts out of live rotation when a healthy fallback exists. CCS persists those quota-triggered pauses across launches, automatically resumes them after the configured cooldown window, and deliberately avoids auto-pausing the last available account so single-account setups still degrade gracefully instead of hard-locking themselves.
- **2026-04-20**: **#1051** Browser automation now defaults safe-off for new installs and upgrades that do not already carry explicit browser settings. CCS changes both Claude Browser Attach and Codex Browser Tools to start with `enabled: false` and `policy: manual`, normalizes missing browser policies on upgrade back to `manual`, preserves explicit existing enablement, and updates status/help/docs so browser tooling is never implied to auto-expose unless users opt in.
- **2026-04-19**: **#1051** Browser tooling now has an explicit exposure policy instead of only coarse enablement toggles. CCS adds `browser.<lane>.policy` (`auto` or `manual`) for both Claude Browser Attach and Codex Browser Tools, exposes CLI-first policy controls through `ccs browser policy`, `ccs browser enable`, and `ccs browser disable`, and adds one-run launch overrides `--browser` and `--no-browser` so users can force browser tooling on or off without editing saved config.
+7 -3
View File
@@ -12,6 +12,10 @@ Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibi
CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with multiple AI providers. CCS manages the binary and configuration automatically.
### Local Backend Choice
CCS defaults to the original `router-for-me/CLIProxyAPI` backend because it is the stable MIT upstream. The `plus` backend is an explicit opt-in path that downloads the community-maintained `kaitranntt/CLIProxyAPIPlus` fork for providers that still require Plus-only support, such as Kiro, GitHub Copilot, Cursor, GitLab, CodeBuddy, and Kilo. CCS does not silently downgrade `backend: plus` to `original`; users choose that backend deliberately when they need those providers.
```
+===========================================================================+
| CLIProxyAPI Integration |
@@ -22,7 +26,7 @@ CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with
| ANTHROPIC_BASE_URL = localhost:XXXX
v
+------------------+
| CLIProxyAPI | Local proxy binary (CLIProxyAPIPlus for kiro/ghcp)
| CLIProxyAPI | Local proxy binary (Plus fork opt-in for plus-only providers)
| (binary) |
+------------------+
|
@@ -73,8 +77,8 @@ CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with
| Gemini | `gemini` | Authorization Code | 9876 | CLIProxyAPI |
| Codex | `codex` | Authorization Code | 9876 | CLIProxyAPI |
| Antigravity | `agy` | Authorization Code | 9876 | CLIProxyAPI |
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus |
| GitHub Copilot | `ghcp` | Device Code | none | CLIProxyAPIPlus |
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus fork |
| GitHub Copilot | `ghcp` | Device Code | none | CLIProxyAPIPlus fork |
### Codex Duplicate-Email Account Identity
+36 -75
View File
@@ -5,17 +5,10 @@
* Pattern: Mirrors npm install behavior (fast check, download only when needed)
*/
import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types';
import {
BACKEND_CONFIG,
DEFAULT_BACKEND,
CLIPROXY_MAX_STABLE_VERSION,
getExecutableName,
} from './platform-detector';
import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
import { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
@@ -23,7 +16,6 @@ import {
UpdateCheckResult,
checkForUpdates,
deleteBinary,
getVersionCachePath,
getBinaryPath,
isBinaryInstalled,
getBinaryInfo,
@@ -38,23 +30,24 @@ import {
} from './binary';
import type { CLIProxyBackend } from './types';
import { getVersionListCachePath } from './binary/version-cache';
export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062';
export const CLIPROXY_PLUS_FORK_URL = 'https://github.com/kaitranntt/CLIProxyAPIPlus';
export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1065';
/**
* Track whether we've already warned the user about the Plus fallback this
* process lifetime. Prevents spamming the warning on every command.
* Track whether we've already shown the optional Plus backend notice this
* process lifetime. Prevents spamming the message on every command.
*/
let plusFallbackWarned = false;
let plusBackendInfoShown = false;
function emitPlusFallbackWarning(): void {
if (plusFallbackWarned) return;
plusFallbackWarned = true;
function emitPlusBackendInfo(): void {
if (plusBackendInfoShown) return;
plusBackendInfoShown = 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. ' +
`${info(
'Using optional community-maintained CLIProxyAPIPlus backend. Stay on ' +
'`backend: original` unless you need plus-only providers. ' +
`Fork: ${CLIPROXY_PLUS_FORK_URL} ` +
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
)}\n`
);
@@ -63,44 +56,26 @@ function emitPlusFallbackWarning(): void {
export function getPlusBackendUnavailableMessage(provider?: string): string {
const prefix = provider
? `${provider} requires CLIProxyAPIPlus,`
: 'CLIProxyAPIPlus upstream repo is currently unavailable,';
: 'This provider requires CLIProxyAPIPlus,';
return (
`${prefix} but local CLIProxy currently supports only \`backend: original\`. ` +
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
`${prefix} which is available through the optional community-maintained ` +
`\`backend: plus\` fork. Set \`cliproxy.backend: plus\` in config.yaml ` +
`only if you need plus-only providers. Fork: ${CLIPROXY_PLUS_FORK_URL}`
);
}
interface ResolveLocalBackendOptions {
notifyOnPlus?: boolean;
}
export function resolveLocalBackend(
backend: CLIProxyBackend = DEFAULT_BACKEND,
options: { warnOnFallback?: boolean } = {}
options: ResolveLocalBackendOptions = {}
): CLIProxyBackend {
if (backend !== 'plus') return backend;
if (options.warnOnFallback) {
emitPlusFallbackWarning();
if (backend === 'plus' && options.notifyOnPlus) {
emitPlusBackendInfo();
}
return 'original';
}
function copyFallbackStateIfMissing(sourcePath: string, targetPath: string): void {
if (!fs.existsSync(sourcePath) || fs.existsSync(targetPath)) return;
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.copyFileSync(sourcePath, targetPath);
}
export function syncPlusFallbackStateIfNeeded(configuredBackend: CLIProxyBackend): void {
if (configuredBackend !== 'plus') return;
const plusDir = getBackendBinDir('plus');
const originalDir = getBackendBinDir('original');
copyFallbackStateIfMissing(
path.join(plusDir, getExecutableName('plus')),
path.join(originalDir, getExecutableName('original'))
);
copyFallbackStateIfMissing(path.join(plusDir, '.version'), path.join(originalDir, '.version'));
copyFallbackStateIfMissing(getVersionPinPath('plus'), getVersionPinPath('original'));
copyFallbackStateIfMissing(getVersionCachePath('plus'), getVersionCachePath('original'));
copyFallbackStateIfMissing(getVersionListCachePath('plus'), getVersionListCachePath('original'));
return backend;
}
function getConfiguredOrDefaultBackend(): CLIProxyBackend {
@@ -117,16 +92,10 @@ export function getStoredConfiguredBackend(): CLIProxyBackend {
}
/**
* Get backend from config, with runtime fallback to 'original' when the user
* still has `backend: plus` saved.
*
* Context (issue #1062): the upstream `router-for-me/CLIProxyAPIPlus` repo was
* deleted, so any `backend: plus` install/update path hits a 404. Rather than
* forcing users to manually edit config.yaml, we degrade to `original` at
* runtime and warn once. This keeps existing installations working without a
* reconfig step, while CCS self-maintains its own Plus fork (future work).
* Get backend from config. Plus remains opt-in and resolves to the CCS-maintained
* community fork instead of degrading to original.
*/
export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend {
export function getConfiguredBackend(options: ResolveLocalBackendOptions = {}): CLIProxyBackend {
return resolveLocalBackend(getConfiguredOrDefaultBackend(), options);
}
@@ -164,8 +133,7 @@ export class BinaryManager {
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
this.backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
this.backend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const defaultConfig = createDefaultConfig(this.backend);
this.config = { ...defaultConfig, ...config };
}
@@ -217,8 +185,7 @@ export async function ensureCLIProxyBinary(
options: EnsureCLIProxyBinaryOptions = {}
): Promise<string> {
const configuredBackend = getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const backend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
// Migrate old shared pin to backend-specific location (one-time migration)
migrateVersionPin(backend);
@@ -250,24 +217,21 @@ export async function ensureCLIProxyBinary(
/** Check if CLIProxyAPI binary is installed */
export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
return new BinaryManager({}, effectiveBackend).isBinaryInstalled();
}
/** Get CLIProxyAPI binary path (may not exist) */
export function getCLIProxyPath(backend?: CLIProxyBackend): string {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
return new BinaryManager({}, effectiveBackend).getBinaryPath();
}
/** Get installed CLIProxyAPI version from .version file */
export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
return readInstalledVersion(
getBackendBinDir(effectiveBackend),
BACKEND_CONFIG[effectiveBackend].fallbackVersion
@@ -294,8 +258,7 @@ export async function installCliproxyVersion(
deps: InstallCliproxyVersionDeps = {}
): Promise<void> {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const manager =
deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ??
new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
@@ -337,8 +300,7 @@ export async function installCliproxyVersion(
/** Fetch the latest CLIProxyAPI version from GitHub API */
export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
return result.latestVersion;
}
@@ -364,8 +326,7 @@ export async function checkCliproxyUpdate(
backend?: CLIProxyBackend
): Promise<CliproxyUpdateCheckResult> {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
// Import isNewerVersion for stability check
+3 -3
View File
@@ -29,14 +29,14 @@ export const VERSION_PIN_FILE = '.version-pin';
* @deprecated Use getGitHubApiUrls(backend) instead
*/
export const GITHUB_API_LATEST_RELEASE =
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases/latest';
'https://api.github.com/repos/kaitranntt/CLIProxyAPIPlus/releases/latest';
export const GITHUB_API_ALL_RELEASES =
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases';
'https://api.github.com/repos/kaitranntt/CLIProxyAPIPlus/releases';
/** GitHub repos per backend */
export const GITHUB_REPOS = {
original: 'router-for-me/CLIProxyAPI',
plus: 'router-for-me/CLIProxyAPIPlus',
plus: 'kaitranntt/CLIProxyAPIPlus',
} as const;
/** Get GitHub API URLs for specific backend */
+2 -2
View File
@@ -351,14 +351,14 @@ export async function execClaudeWithCLIProxy(
}
if (!useRemoteProxy) {
localBackend = getConfiguredBackend({ warnOnFallback: true });
localBackend = getConfiguredBackend({ notifyOnPlus: 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`);
throw new Error(`Provider ${p} requires local CLIProxy Plus backend`);
}
}
}
+5 -6
View File
@@ -22,20 +22,19 @@ export const BACKEND_CONFIG = {
fallbackVersion: '6.8.2',
},
plus: {
repo: 'router-for-me/CLIProxyAPIPlus',
repo: 'kaitranntt/CLIProxyAPIPlus',
binaryPrefix: 'CLIProxyAPIPlus',
executable: 'cli-proxy-api-plus',
fallbackVersion: '6.8.2-0',
fallbackVersion: '6.9.36-0',
},
} as const;
/**
* Default backend
*
* Set to 'original' because upstream `router-for-me/CLIProxyAPIPlus` was
* deleted (issue #1062). The original `router-for-me/CLIProxyAPI` repo is
* still maintained. Users with existing `backend: plus` configs are migrated
* at runtime via a 404 fallback in the installer (see binary/installer.ts).
* Keep 'original' as the stable default. The Plus backend is available as an
* opt-in community-maintained fork for providers that are not available in the
* original upstream binary.
*/
export const DEFAULT_BACKEND: CLIProxyBackend = 'original';
+7 -15
View File
@@ -19,7 +19,6 @@ import {
clearPinnedVersion,
isVersionPinned,
resolveLocalBackend,
syncPlusFallbackStateIfNeeded,
} from '../binary-manager';
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
import { CLIProxyBackend } from '../types';
@@ -58,8 +57,7 @@ export interface LatestVersionResult {
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const backendConfig = BACKEND_CONFIG[effectiveBackend];
return {
installed: isCLIProxyInstalled(effectiveBackend),
@@ -77,8 +75,7 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> {
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
try {
// Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo)
@@ -127,8 +124,7 @@ export async function installVersion(
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
try {
await installCliproxyVersion(version, verbose, effectiveBackend);
@@ -157,8 +153,7 @@ export async function installLatest(
): Promise<InstallResult> {
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
try {
const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend);
@@ -196,8 +191,7 @@ export async function installLatest(
export function isPinned(backend?: CLIProxyBackend): boolean {
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
return isVersionPinned(effectiveBackend);
}
@@ -207,8 +201,7 @@ export function isPinned(backend?: CLIProxyBackend): boolean {
export function getPinned(backend?: CLIProxyBackend): string | null {
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
return getPinnedVersion(effectiveBackend);
}
@@ -218,7 +211,6 @@ export function getPinned(backend?: CLIProxyBackend): string | null {
export function clearPin(backend?: CLIProxyBackend): void {
const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend);
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
clearPinnedVersion(effectiveBackend);
}
+2 -2
View File
@@ -79,8 +79,8 @@ export function validateProfileName(name: string): string | null {
/**
* Validate provider/backend compatibility
* Returns error message if a provider requires Plus while local CLIProxy is
* running with the fallbacked original backend.
* Returns an error message if a provider requires Plus while local CLIProxy is
* configured for the default original backend.
*/
export function validateProviderBackend(provider: CLIProxyProfileName): string | null {
// Normalize provider to lowercase for case-insensitive comparison
+3 -4
View File
@@ -148,10 +148,9 @@ export type CLIProxyProvider =
/**
* CLIProxy backend selection
* - original: CLIProxyAPI (current default; upstream still maintained)
* - plus: CLIProxyAPIPlus (expanded provider support; upstream deleted as of
* issue #1062 runtime falls back to `original`. Retained for forward
* compatibility once CCS self-maintains its own Plus build.)
* - original: CLIProxyAPI (default; upstream maintained by router-for-me)
* - plus: CCS-maintained CLIProxyAPIPlus community fork for opt-in providers
* that are not available in the original backend
*/
export type CLIProxyBackend = 'original' | 'plus';
+2 -6
View File
@@ -86,7 +86,7 @@ export async function showHelp(): Promise<void> {
[
[
'--backend <type>',
'Use specific backend: original | plus (local default: original; plus currently falls back locally)',
'Use specific backend: original | plus (default: original; plus uses community fork)',
],
['--target <cli>', 'Default target for created/edited variants: claude | droid'],
['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'],
@@ -105,11 +105,7 @@ export async function showHelp(): Promise<void> {
console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.'));
console.log(dim(' Routing: use gcli/<model> or agy/<model> to keep overlapping models pinned.'));
console.log(
dim(
' Backend: local CLIProxy currently uses original by default; saved plus configs fall back locally.'
)
);
console.log(dim(' Backend: original is the default; plus is opt-in for plus-only providers.'));
console.log('');
console.log(subheader('Notes:'));
console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`);
@@ -1,8 +1,4 @@
import {
installCliproxyVersion,
resolveLocalBackend,
syncPlusFallbackStateIfNeeded,
} 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';
@@ -56,8 +52,7 @@ export async function installDashboardCliproxyVersion(
backend: CLIProxyBackend,
deps: InstallDashboardCliproxyVersionDeps = defaultDeps
): Promise<DashboardCliproxyInstallResult> {
syncPlusFallbackStateIfNeeded(backend);
const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true });
const effectiveBackend = resolveLocalBackend(backend, { notifyOnPlus: true });
const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const shouldRestoreService = await wasProxyRunning(deps);
@@ -20,7 +20,7 @@ describe('Backend Selection', () => {
it('has correct configuration for plus backend', () => {
const config = platformDetector.BACKEND_CONFIG.plus;
assert.strictEqual(config.repo, 'router-for-me/CLIProxyAPIPlus');
assert.strictEqual(config.repo, 'kaitranntt/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');
@@ -28,7 +28,7 @@ describe('Backend Selection', () => {
});
describe('DEFAULT_BACKEND', () => {
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => {
it('defaults to original backend', () => {
assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'original');
});
});
@@ -45,7 +45,7 @@ describe('Backend Selection', () => {
assert(info.binaryName.startsWith('CLIProxyAPIPlus_6.6.51-0_'));
});
it('uses original backend by default (Plus upstream deleted, issue #1062)', () => {
it('uses original backend by default', () => {
const info = platformDetector.detectPlatform();
assert(info.binaryName.startsWith('CLIProxyAPI_'));
assert(!info.binaryName.includes('CLIProxyAPIPlus'));
@@ -77,7 +77,7 @@ describe('Backend Selection', () => {
assert.strictEqual(name, expected);
});
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => {
it('defaults to original backend', () => {
const name = platformDetector.getExecutableName();
const expected = isWindows ? 'cli-proxy-api.exe' : 'cli-proxy-api';
assert.strictEqual(name, expected);
@@ -93,10 +93,10 @@ describe('Backend Selection', () => {
it('uses correct repo for plus backend', () => {
const url = platformDetector.getDownloadUrl('6.6.51-0', 'plus');
assert(url.includes('router-for-me/CLIProxyAPIPlus/releases'));
assert(url.includes('kaitranntt/CLIProxyAPIPlus/releases'));
});
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => {
it('defaults to original backend', () => {
const url = platformDetector.getDownloadUrl();
assert(url.includes('router-for-me/CLIProxyAPI/releases'));
assert(!url.includes('CLIProxyAPIPlus'));
@@ -25,7 +25,7 @@ afterEach(() => {
});
describe('installCliproxyVersion', () => {
it('degrades explicit plus backend requests to original before install flows run', async () => {
it('keeps explicit plus backend requests on plus before install flows run', async () => {
let seenBackend: string | undefined;
const binaryManager = await import(
@@ -38,7 +38,7 @@ describe('installCliproxyVersion', () => {
return {
isBinaryInstalled: () => false,
deleteBinary: () => undefined,
ensureBinary: async () => '/tmp/ccs-bin/original/cliproxy',
ensureBinary: async () => '/tmp/ccs-bin/plus/cliproxy',
};
},
stopProxyFn: async () => ({ stopped: false, error: 'No active CLIProxy session found' }),
@@ -48,10 +48,10 @@ describe('installCliproxyVersion', () => {
getInstalledVersion: () => '6.6.80',
});
expect(seenBackend).toBe('original');
expect(seenBackend).toBe('plus');
});
it('returns original and emits a real warning when plus backend is resolved locally', async () => {
it('returns plus and emits an informational notice when plus backend is resolved locally', async () => {
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-warning=${Date.now()}`
);
@@ -64,16 +64,17 @@ describe('installCliproxyVersion', () => {
}) as typeof process.stderr.write;
try {
expect(binaryManager.resolveLocalBackend('plus', { warnOnFallback: true })).toBe('original');
expect(binaryManager.resolveLocalBackend('plus', { notifyOnPlus: true })).toBe('plus');
} finally {
process.stderr.write = originalWrite;
}
expect(writes.join('')).toContain('CLIProxyAPIPlus upstream repo is currently unavailable');
expect(writes.join('')).toContain('optional community-maintained CLIProxyAPIPlus backend');
expect(writes.join('')).toContain('backend: original');
expect(writes.join('')).toContain('kaitranntt/CLIProxyAPIPlus');
});
it('reuses plus binary and pin state when local runtime falls back to original', async () => {
it('uses plus binary and pin state when local runtime is configured for plus', async () => {
const { createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types');
const { saveUnifiedConfig } = await import('../../../src/config/unified-config-loader');
const { savePinnedVersion } = await import('../../../src/cliproxy/binary/version-cache');
@@ -96,7 +97,7 @@ describe('installCliproxyVersion', () => {
expect(status.installed).toBe(true);
expect(status.pinnedVersion).toBe('6.6.80-0');
expect(status.binaryPath).toContain('/original/');
expect(status.binaryPath).toContain('/plus/');
});
it('attempts to stop the proxy even when there is no tracked running session', async () => {
@@ -81,6 +81,12 @@ cliproxy:
}
});
function setConfiguredBackend(backend: 'original' | 'plus'): void {
const configPath = path.join(tmpDir, 'config.yaml');
const current = fs.readFileSync(configPath, 'utf-8');
fs.writeFileSync(configPath, current.replace(/backend: (original|plus)/, `backend: ${backend}`));
}
it('rejects provider change without model update', () => {
const result = updateVariant('demo', { provider: 'codex' });
expect(result.success).toBe(false);
@@ -96,13 +102,17 @@ cliproxy:
expect(result.error).toContain('denylist');
});
it('reports plus-only providers as temporarily unavailable on local CLIProxy', () => {
it('reports plus-only providers as requiring the optional plus backend', () => {
setConfiguredBackend('original');
const error = validateProviderBackend('ghcp');
expect(error).toContain('currently supports only `backend: original`');
expect(error).toContain('issues/1062');
expect(error).toContain('`backend: plus`');
expect(error).toContain('kaitranntt/CLIProxyAPIPlus');
});
it('leaves the settings file unchanged when a plus-only provider update is rejected', () => {
setConfiguredBackend('original');
const settingsPath = path.join(tmpDir, 'gemini-demo.settings.json');
const before = fs.readFileSync(settingsPath, 'utf-8');
@@ -47,7 +47,7 @@ function createDeps(
}
describe('installDashboardCliproxyVersion', () => {
it('restarts the proxy after install when it was already running', async () => {
it('restarts the plus proxy after install when it was already running', async () => {
const { deps, calls } = createDeps({ sessionRunning: true });
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
@@ -56,14 +56,14 @@ describe('installDashboardCliproxyVersion', () => {
success: true,
restarted: true,
port: 8317,
message: 'Successfully installed CLIProxy v6.7.1 and restarted it on port 8317',
message: 'Successfully installed CLIProxy Plus v6.7.1 and restarted it on port 8317',
});
expect(calls.isCliproxyRunning).toBe(0);
expect(calls.installCliproxyVersion).toBe(1);
expect(calls.ensureCliproxyService).toBe(1);
});
it('keeps the proxy stopped after install when it was not running beforehand', async () => {
it('keeps the plus proxy stopped after install when it was not running beforehand', async () => {
const { deps, calls } = createDeps({ sessionRunning: false, remoteRunning: false });
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
@@ -71,7 +71,7 @@ describe('installDashboardCliproxyVersion', () => {
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: true,
restarted: false,
message: 'Successfully installed CLIProxy v6.7.1',
message: 'Successfully installed CLIProxy Plus 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 v6.7.1, but restart failed',
message: 'Installed CLIProxy v6.7.1, but failed to restart it',
error: 'Installed CLIProxy Plus v6.7.1, but restart failed',
message: 'Installed CLIProxy Plus v6.7.1, but failed to restart it',
});
});
});
@@ -96,7 +96,7 @@ afterAll(async () => {
});
describe('cliproxy-stats-routes install contract', () => {
it('routes saved plus configs through original backend for update checks', async () => {
it('keeps saved plus configs on the plus backend for update checks', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/update-check`);
expect(response.status).toBe(200);
@@ -107,8 +107,8 @@ describe('cliproxy-stats-routes install contract', () => {
latestVersion: string;
};
expect(body.backend).toBe('original');
expect(body.backendLabel).toBe('CLIProxy');
expect(body.backend).toBe('plus');
expect(body.backendLabel).toBe('CLIProxy Plus');
expect(body.currentVersion).toBe('6.6.80');
expect(body.latestVersion).toBe('6.6.89');
});