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 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 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: Deep dive:
[CLIProxy API](https://docs.ccs.kaitran.ca/features/proxy/cliproxy-api). [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 ### 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-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-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. - **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. 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 | | CLIProxyAPI Integration |
@@ -22,7 +26,7 @@ CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with
| ANTHROPIC_BASE_URL = localhost:XXXX | ANTHROPIC_BASE_URL = localhost:XXXX
v v
+------------------+ +------------------+
| CLIProxyAPI | Local proxy binary (CLIProxyAPIPlus for kiro/ghcp) | CLIProxyAPI | Local proxy binary (Plus fork opt-in for plus-only providers)
| (binary) | | (binary) |
+------------------+ +------------------+
| |
@@ -73,8 +77,8 @@ CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with
| Gemini | `gemini` | Authorization Code | 9876 | CLIProxyAPI | | Gemini | `gemini` | Authorization Code | 9876 | CLIProxyAPI |
| Codex | `codex` | Authorization Code | 9876 | CLIProxyAPI | | Codex | `codex` | Authorization Code | 9876 | CLIProxyAPI |
| Antigravity | `agy` | Authorization Code | 9876 | CLIProxyAPI | | Antigravity | `agy` | Authorization Code | 9876 | CLIProxyAPI |
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus | | Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus fork |
| GitHub Copilot | `ghcp` | Device Code | none | CLIProxyAPIPlus | | GitHub Copilot | `ghcp` | Device Code | none | CLIProxyAPIPlus fork |
### Codex Duplicate-Email Account Identity ### Codex Duplicate-Email Account Identity
+36 -75
View File
@@ -5,17 +5,10 @@
* Pattern: Mirrors npm install behavior (fast check, download only when needed) * 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 { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator'; import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types'; import { BinaryInfo, BinaryManagerConfig } from './types';
import { import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
BACKEND_CONFIG,
DEFAULT_BACKEND,
CLIPROXY_MAX_STABLE_VERSION,
getExecutableName,
} from './platform-detector';
import { stopProxy } from './services/proxy-lifecycle-service'; import { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils'; import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
@@ -23,7 +16,6 @@ import {
UpdateCheckResult, UpdateCheckResult,
checkForUpdates, checkForUpdates,
deleteBinary, deleteBinary,
getVersionCachePath,
getBinaryPath, getBinaryPath,
isBinaryInstalled, isBinaryInstalled,
getBinaryInfo, getBinaryInfo,
@@ -38,23 +30,24 @@ import {
} from './binary'; } from './binary';
import type { CLIProxyBackend } from './types'; 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 * Track whether we've already shown the optional Plus backend notice this
* process lifetime. Prevents spamming the warning on every command. * process lifetime. Prevents spamming the message on every command.
*/ */
let plusFallbackWarned = false; let plusBackendInfoShown = false;
function emitPlusFallbackWarning(): void { function emitPlusBackendInfo(): void {
if (plusFallbackWarned) return; if (plusBackendInfoShown) return;
plusFallbackWarned = true; plusBackendInfoShown = true;
process.stderr.write( process.stderr.write(
`${warn( `${info(
'CLIProxyAPIPlus upstream repo is currently unavailable; local CLIProxy is falling back to ' + 'Using optional community-maintained CLIProxyAPIPlus backend. Stay on ' +
'`backend: original`. Run `ccs config` to update your saved config. ' + '`backend: original` unless you need plus-only providers. ' +
`Fork: ${CLIPROXY_PLUS_FORK_URL} ` +
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}` `Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
)}\n` )}\n`
); );
@@ -63,44 +56,26 @@ function emitPlusFallbackWarning(): void {
export function getPlusBackendUnavailableMessage(provider?: string): string { export function getPlusBackendUnavailableMessage(provider?: string): string {
const prefix = provider const prefix = provider
? `${provider} requires CLIProxyAPIPlus,` ? `${provider} requires CLIProxyAPIPlus,`
: 'CLIProxyAPIPlus upstream repo is currently unavailable,'; : 'This provider requires CLIProxyAPIPlus,';
return ( return (
`${prefix} but local CLIProxy currently supports only \`backend: original\`. ` + `${prefix} which is available through the optional community-maintained ` +
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}` `\`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( export function resolveLocalBackend(
backend: CLIProxyBackend = DEFAULT_BACKEND, backend: CLIProxyBackend = DEFAULT_BACKEND,
options: { warnOnFallback?: boolean } = {} options: ResolveLocalBackendOptions = {}
): CLIProxyBackend { ): CLIProxyBackend {
if (backend !== 'plus') return backend; if (backend === 'plus' && options.notifyOnPlus) {
if (options.warnOnFallback) { emitPlusBackendInfo();
emitPlusFallbackWarning();
} }
return 'original'; return backend;
}
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'));
} }
function getConfiguredOrDefaultBackend(): CLIProxyBackend { function getConfiguredOrDefaultBackend(): CLIProxyBackend {
@@ -117,16 +92,10 @@ export function getStoredConfiguredBackend(): CLIProxyBackend {
} }
/** /**
* Get backend from config, with runtime fallback to 'original' when the user * Get backend from config. Plus remains opt-in and resolves to the CCS-maintained
* still has `backend: plus` saved. * community fork instead of degrading to original.
*
* 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).
*/ */
export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend { export function getConfiguredBackend(options: ResolveLocalBackendOptions = {}): CLIProxyBackend {
return resolveLocalBackend(getConfiguredOrDefaultBackend(), options); return resolveLocalBackend(getConfiguredOrDefaultBackend(), options);
} }
@@ -164,8 +133,7 @@ export class BinaryManager {
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) { constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); this.backend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
this.backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const defaultConfig = createDefaultConfig(this.backend); const defaultConfig = createDefaultConfig(this.backend);
this.config = { ...defaultConfig, ...config }; this.config = { ...defaultConfig, ...config };
} }
@@ -217,8 +185,7 @@ export async function ensureCLIProxyBinary(
options: EnsureCLIProxyBinaryOptions = {} options: EnsureCLIProxyBinaryOptions = {}
): Promise<string> { ): Promise<string> {
const configuredBackend = getConfiguredOrDefaultBackend(); const configuredBackend = getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const backend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
// Migrate old shared pin to backend-specific location (one-time migration) // Migrate old shared pin to backend-specific location (one-time migration)
migrateVersionPin(backend); migrateVersionPin(backend);
@@ -250,24 +217,21 @@ export async function ensureCLIProxyBinary(
/** Check if CLIProxyAPI binary is installed */ /** Check if CLIProxyAPI binary is installed */
export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean { export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return new BinaryManager({}, effectiveBackend).isBinaryInstalled(); return new BinaryManager({}, effectiveBackend).isBinaryInstalled();
} }
/** Get CLIProxyAPI binary path (may not exist) */ /** Get CLIProxyAPI binary path (may not exist) */
export function getCLIProxyPath(backend?: CLIProxyBackend): string { export function getCLIProxyPath(backend?: CLIProxyBackend): string {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return new BinaryManager({}, effectiveBackend).getBinaryPath(); return new BinaryManager({}, effectiveBackend).getBinaryPath();
} }
/** Get installed CLIProxyAPI version from .version file */ /** Get installed CLIProxyAPI version from .version file */
export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string { export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return readInstalledVersion( return readInstalledVersion(
getBackendBinDir(effectiveBackend), getBackendBinDir(effectiveBackend),
BACKEND_CONFIG[effectiveBackend].fallbackVersion BACKEND_CONFIG[effectiveBackend].fallbackVersion
@@ -294,8 +258,7 @@ export async function installCliproxyVersion(
deps: InstallCliproxyVersionDeps = {} deps: InstallCliproxyVersionDeps = {}
): Promise<void> { ): Promise<void> {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const manager = const manager =
deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ?? deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ??
new BinaryManager({ 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 */ /** Fetch the latest CLIProxyAPI version from GitHub API */
export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> { export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates(); const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
return result.latestVersion; return result.latestVersion;
} }
@@ -364,8 +326,7 @@ export async function checkCliproxyUpdate(
backend?: CLIProxyBackend backend?: CLIProxyBackend
): Promise<CliproxyUpdateCheckResult> { ): Promise<CliproxyUpdateCheckResult> {
const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates(); const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
// Import isNewerVersion for stability check // 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 * @deprecated Use getGitHubApiUrls(backend) instead
*/ */
export const GITHUB_API_LATEST_RELEASE = 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 = 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 */ /** GitHub repos per backend */
export const GITHUB_REPOS = { export const GITHUB_REPOS = {
original: 'router-for-me/CLIProxyAPI', original: 'router-for-me/CLIProxyAPI',
plus: 'router-for-me/CLIProxyAPIPlus', plus: 'kaitranntt/CLIProxyAPIPlus',
} as const; } as const;
/** Get GitHub API URLs for specific backend */ /** Get GitHub API URLs for specific backend */
+2 -2
View File
@@ -351,14 +351,14 @@ export async function execClaudeWithCLIProxy(
} }
if (!useRemoteProxy) { if (!useRemoteProxy) {
localBackend = getConfiguredBackend({ warnOnFallback: true }); localBackend = getConfiguredBackend({ notifyOnPlus: true });
for (const p of allProviders) { for (const p of allProviders) {
if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) { if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
console.error(''); console.error('');
console.error(fail(getPlusBackendUnavailableMessage(p))); console.error(fail(getPlusBackendUnavailableMessage(p)));
console.error(''); 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', fallbackVersion: '6.8.2',
}, },
plus: { plus: {
repo: 'router-for-me/CLIProxyAPIPlus', repo: 'kaitranntt/CLIProxyAPIPlus',
binaryPrefix: 'CLIProxyAPIPlus', binaryPrefix: 'CLIProxyAPIPlus',
executable: 'cli-proxy-api-plus', executable: 'cli-proxy-api-plus',
fallbackVersion: '6.8.2-0', fallbackVersion: '6.9.36-0',
}, },
} as const; } as const;
/** /**
* Default backend * Default backend
* *
* Set to 'original' because upstream `router-for-me/CLIProxyAPIPlus` was * Keep 'original' as the stable default. The Plus backend is available as an
* deleted (issue #1062). The original `router-for-me/CLIProxyAPI` repo is * opt-in community-maintained fork for providers that are not available in the
* still maintained. Users with existing `backend: plus` configs are migrated * original upstream binary.
* at runtime via a 404 fallback in the installer (see binary/installer.ts).
*/ */
export const DEFAULT_BACKEND: CLIProxyBackend = 'original'; export const DEFAULT_BACKEND: CLIProxyBackend = 'original';
+7 -15
View File
@@ -19,7 +19,6 @@ import {
clearPinnedVersion, clearPinnedVersion,
isVersionPinned, isVersionPinned,
resolveLocalBackend, resolveLocalBackend,
syncPlusFallbackStateIfNeeded,
} from '../binary-manager'; } from '../binary-manager';
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector'; import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
import { CLIProxyBackend } from '../types'; import { CLIProxyBackend } from '../types';
@@ -58,8 +57,7 @@ export interface LatestVersionResult {
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult { export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const backendConfig = BACKEND_CONFIG[effectiveBackend]; const backendConfig = BACKEND_CONFIG[effectiveBackend];
return { return {
installed: isCLIProxyInstalled(effectiveBackend), installed: isCLIProxyInstalled(effectiveBackend),
@@ -77,8 +75,7 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> { export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> {
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
try { try {
// Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo) // Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo)
@@ -127,8 +124,7 @@ export async function installVersion(
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
try { try {
await installCliproxyVersion(version, verbose, effectiveBackend); await installCliproxyVersion(version, verbose, effectiveBackend);
@@ -157,8 +153,7 @@ export async function installLatest(
): Promise<InstallResult> { ): Promise<InstallResult> {
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
try { try {
const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend); const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend);
@@ -196,8 +191,7 @@ export async function installLatest(
export function isPinned(backend?: CLIProxyBackend): boolean { export function isPinned(backend?: CLIProxyBackend): boolean {
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return isVersionPinned(effectiveBackend); return isVersionPinned(effectiveBackend);
} }
@@ -207,8 +201,7 @@ export function isPinned(backend?: CLIProxyBackend): boolean {
export function getPinned(backend?: CLIProxyBackend): string | null { export function getPinned(backend?: CLIProxyBackend): string | null {
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return getPinnedVersion(effectiveBackend); return getPinnedVersion(effectiveBackend);
} }
@@ -218,7 +211,6 @@ export function getPinned(backend?: CLIProxyBackend): string | null {
export function clearPin(backend?: CLIProxyBackend): void { export function clearPin(backend?: CLIProxyBackend): void {
const configuredBackend = const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
syncPlusFallbackStateIfNeeded(configuredBackend); const effectiveBackend = resolveLocalBackend(configuredBackend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
clearPinnedVersion(effectiveBackend); clearPinnedVersion(effectiveBackend);
} }
+2 -2
View File
@@ -79,8 +79,8 @@ export function validateProfileName(name: string): string | null {
/** /**
* Validate provider/backend compatibility * Validate provider/backend compatibility
* Returns error message if a provider requires Plus while local CLIProxy is * Returns an error message if a provider requires Plus while local CLIProxy is
* running with the fallbacked original backend. * configured for the default original backend.
*/ */
export function validateProviderBackend(provider: CLIProxyProfileName): string | null { export function validateProviderBackend(provider: CLIProxyProfileName): string | null {
// Normalize provider to lowercase for case-insensitive comparison // Normalize provider to lowercase for case-insensitive comparison
+3 -4
View File
@@ -148,10 +148,9 @@ export type CLIProxyProvider =
/** /**
* CLIProxy backend selection * CLIProxy backend selection
* - original: CLIProxyAPI (current default; upstream still maintained) * - original: CLIProxyAPI (default; upstream maintained by router-for-me)
* - plus: CLIProxyAPIPlus (expanded provider support; upstream deleted as of * - plus: CCS-maintained CLIProxyAPIPlus community fork for opt-in providers
* issue #1062 — runtime falls back to `original`. Retained for forward * that are not available in the original backend
* compatibility once CCS self-maintains its own Plus build.)
*/ */
export type CLIProxyBackend = 'original' | 'plus'; export type CLIProxyBackend = 'original' | 'plus';
+2 -6
View File
@@ -86,7 +86,7 @@ export async function showHelp(): Promise<void> {
[ [
[ [
'--backend <type>', '--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'], ['--target <cli>', 'Default target for created/edited variants: claude | droid'],
['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'], ['--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(' 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(' Routing: use gcli/<model> or agy/<model> to keep overlapping models pinned.'));
console.log( console.log(dim(' Backend: original is the default; plus is opt-in for plus-only providers.'));
dim(
' Backend: local CLIProxy currently uses original by default; saved plus configs fall back locally.'
)
);
console.log(''); console.log('');
console.log(subheader('Notes:')); console.log(subheader('Notes:'));
console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`); console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`);
@@ -1,8 +1,4 @@
import { import { installCliproxyVersion, resolveLocalBackend } from '../../cliproxy/binary-manager';
installCliproxyVersion,
resolveLocalBackend,
syncPlusFallbackStateIfNeeded,
} from '../../cliproxy/binary-manager';
import { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/service-manager'; import { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/service-manager';
import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker'; import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker';
import { isCliproxyRunning } from '../../cliproxy/stats-fetcher'; import { isCliproxyRunning } from '../../cliproxy/stats-fetcher';
@@ -56,8 +52,7 @@ export async function installDashboardCliproxyVersion(
backend: CLIProxyBackend, backend: CLIProxyBackend,
deps: InstallDashboardCliproxyVersionDeps = defaultDeps deps: InstallDashboardCliproxyVersionDeps = defaultDeps
): Promise<DashboardCliproxyInstallResult> { ): Promise<DashboardCliproxyInstallResult> {
syncPlusFallbackStateIfNeeded(backend); const effectiveBackend = resolveLocalBackend(backend, { notifyOnPlus: true });
const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true });
const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const shouldRestoreService = await wasProxyRunning(deps); const shouldRestoreService = await wasProxyRunning(deps);
@@ -20,7 +20,7 @@ describe('Backend Selection', () => {
it('has correct configuration for plus backend', () => { it('has correct configuration for plus backend', () => {
const config = platformDetector.BACKEND_CONFIG.plus; 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.binaryPrefix, 'CLIProxyAPIPlus');
assert.strictEqual(config.executable, 'cli-proxy-api-plus'); assert.strictEqual(config.executable, 'cli-proxy-api-plus');
assert(config.fallbackVersion.match(/^\d+\.\d+\.\d+-\d+$/), 'plus version has -0 suffix'); assert(config.fallbackVersion.match(/^\d+\.\d+\.\d+-\d+$/), 'plus version has -0 suffix');
@@ -28,7 +28,7 @@ describe('Backend Selection', () => {
}); });
describe('DEFAULT_BACKEND', () => { describe('DEFAULT_BACKEND', () => {
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => { it('defaults to original backend', () => {
assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'original'); assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'original');
}); });
}); });
@@ -45,7 +45,7 @@ describe('Backend Selection', () => {
assert(info.binaryName.startsWith('CLIProxyAPIPlus_6.6.51-0_')); 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(); const info = platformDetector.detectPlatform();
assert(info.binaryName.startsWith('CLIProxyAPI_')); assert(info.binaryName.startsWith('CLIProxyAPI_'));
assert(!info.binaryName.includes('CLIProxyAPIPlus')); assert(!info.binaryName.includes('CLIProxyAPIPlus'));
@@ -77,7 +77,7 @@ describe('Backend Selection', () => {
assert.strictEqual(name, expected); assert.strictEqual(name, expected);
}); });
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => { it('defaults to original backend', () => {
const name = platformDetector.getExecutableName(); const name = platformDetector.getExecutableName();
const expected = isWindows ? 'cli-proxy-api.exe' : 'cli-proxy-api'; const expected = isWindows ? 'cli-proxy-api.exe' : 'cli-proxy-api';
assert.strictEqual(name, expected); assert.strictEqual(name, expected);
@@ -93,10 +93,10 @@ describe('Backend Selection', () => {
it('uses correct repo for plus backend', () => { it('uses correct repo for plus backend', () => {
const url = platformDetector.getDownloadUrl('6.6.51-0', 'plus'); 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(); const url = platformDetector.getDownloadUrl();
assert(url.includes('router-for-me/CLIProxyAPI/releases')); assert(url.includes('router-for-me/CLIProxyAPI/releases'));
assert(!url.includes('CLIProxyAPIPlus')); assert(!url.includes('CLIProxyAPIPlus'));
@@ -25,7 +25,7 @@ afterEach(() => {
}); });
describe('installCliproxyVersion', () => { 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; let seenBackend: string | undefined;
const binaryManager = await import( const binaryManager = await import(
@@ -38,7 +38,7 @@ describe('installCliproxyVersion', () => {
return { return {
isBinaryInstalled: () => false, isBinaryInstalled: () => false,
deleteBinary: () => undefined, 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' }), stopProxyFn: async () => ({ stopped: false, error: 'No active CLIProxy session found' }),
@@ -48,10 +48,10 @@ describe('installCliproxyVersion', () => {
getInstalledVersion: () => '6.6.80', 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( const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-warning=${Date.now()}` `../../../src/cliproxy/binary-manager?binary-manager-warning=${Date.now()}`
); );
@@ -64,16 +64,17 @@ describe('installCliproxyVersion', () => {
}) as typeof process.stderr.write; }) as typeof process.stderr.write;
try { try {
expect(binaryManager.resolveLocalBackend('plus', { warnOnFallback: true })).toBe('original'); expect(binaryManager.resolveLocalBackend('plus', { notifyOnPlus: true })).toBe('plus');
} finally { } finally {
process.stderr.write = originalWrite; 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('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 { createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types');
const { saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'); const { saveUnifiedConfig } = await import('../../../src/config/unified-config-loader');
const { savePinnedVersion } = await import('../../../src/cliproxy/binary/version-cache'); const { savePinnedVersion } = await import('../../../src/cliproxy/binary/version-cache');
@@ -96,7 +97,7 @@ describe('installCliproxyVersion', () => {
expect(status.installed).toBe(true); expect(status.installed).toBe(true);
expect(status.pinnedVersion).toBe('6.6.80-0'); 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 () => { 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', () => { it('rejects provider change without model update', () => {
const result = updateVariant('demo', { provider: 'codex' }); const result = updateVariant('demo', { provider: 'codex' });
expect(result.success).toBe(false); expect(result.success).toBe(false);
@@ -96,13 +102,17 @@ cliproxy:
expect(result.error).toContain('denylist'); 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'); const error = validateProviderBackend('ghcp');
expect(error).toContain('currently supports only `backend: original`'); expect(error).toContain('`backend: plus`');
expect(error).toContain('issues/1062'); expect(error).toContain('kaitranntt/CLIProxyAPIPlus');
}); });
it('leaves the settings file unchanged when a plus-only provider update is rejected', () => { 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 settingsPath = path.join(tmpDir, 'gemini-demo.settings.json');
const before = fs.readFileSync(settingsPath, 'utf-8'); const before = fs.readFileSync(settingsPath, 'utf-8');
@@ -47,7 +47,7 @@ function createDeps(
} }
describe('installDashboardCliproxyVersion', () => { 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 { deps, calls } = createDeps({ sessionRunning: true });
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps); const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
@@ -56,14 +56,14 @@ describe('installDashboardCliproxyVersion', () => {
success: true, success: true,
restarted: true, restarted: true,
port: 8317, 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.isCliproxyRunning).toBe(0);
expect(calls.installCliproxyVersion).toBe(1); expect(calls.installCliproxyVersion).toBe(1);
expect(calls.ensureCliproxyService).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 { deps, calls } = createDeps({ sessionRunning: false, remoteRunning: false });
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps); const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
@@ -71,7 +71,7 @@ describe('installDashboardCliproxyVersion', () => {
expect(result).toEqual<DashboardCliproxyInstallResult>({ expect(result).toEqual<DashboardCliproxyInstallResult>({
success: true, success: true,
restarted: false, restarted: false,
message: 'Successfully installed CLIProxy v6.7.1', message: 'Successfully installed CLIProxy Plus v6.7.1',
}); });
expect(calls.isCliproxyRunning).toBe(1); expect(calls.isCliproxyRunning).toBe(1);
expect(calls.installCliproxyVersion).toBe(1); expect(calls.installCliproxyVersion).toBe(1);
@@ -118,8 +118,8 @@ describe('installDashboardCliproxyVersion', () => {
expect(result).toEqual<DashboardCliproxyInstallResult>({ expect(result).toEqual<DashboardCliproxyInstallResult>({
success: false, success: false,
restarted: false, restarted: false,
error: 'Installed CLIProxy v6.7.1, but restart failed', error: 'Installed CLIProxy Plus v6.7.1, but restart failed',
message: 'Installed CLIProxy v6.7.1, but failed to restart it', message: 'Installed CLIProxy Plus v6.7.1, but failed to restart it',
}); });
}); });
}); });
@@ -96,7 +96,7 @@ afterAll(async () => {
}); });
describe('cliproxy-stats-routes install contract', () => { 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`); const response = await fetch(`${baseUrl}/api/cliproxy/update-check`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -107,8 +107,8 @@ describe('cliproxy-stats-routes install contract', () => {
latestVersion: string; latestVersion: string;
}; };
expect(body.backend).toBe('original'); expect(body.backend).toBe('plus');
expect(body.backendLabel).toBe('CLIProxy'); expect(body.backendLabel).toBe('CLIProxy Plus');
expect(body.currentVersion).toBe('6.6.80'); expect(body.currentVersion).toBe('6.6.80');
expect(body.latestVersion).toBe('6.6.89'); expect(body.latestVersion).toBe('6.6.89');
}); });