From 3d22bb07f0d551959498c76939ba83dd4ee1d948 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 23 Apr 2026 21:25:29 -0400 Subject: [PATCH] fix(cliproxy): route plus backend to maintained fork --- README.md | 3 +- docs/project-roadmap.md | 1 + docs/system-architecture/provider-flows.md | 10 +- src/cliproxy/binary-manager.ts | 111 ++++++------------ src/cliproxy/binary/types.ts | 6 +- src/cliproxy/executor/index.ts | 4 +- src/cliproxy/platform-detector.ts | 11 +- src/cliproxy/services/binary-service.ts | 22 ++-- src/cliproxy/services/variant-service.ts | 4 +- src/cliproxy/types.ts | 7 +- src/commands/cliproxy/help-subcommand.ts | 8 +- .../cliproxy-dashboard-install-service.ts | 9 +- tests/unit/cliproxy/backend-selection.test.js | 12 +- .../cliproxy/binary-manager-install.test.ts | 17 +-- .../cliproxy/variant-update-service.test.ts | 16 ++- ...cliproxy-dashboard-install-service.test.ts | 12 +- .../cliproxy-stats-routes-install.test.ts | 6 +- 17 files changed, 109 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index a4b71e6a..5a1480a0 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index 6b76df05..a07dcae8 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -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..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. diff --git a/docs/system-architecture/provider-flows.md b/docs/system-architecture/provider-flows.md index a1853c49..9caf91c7 100644 --- a/docs/system-architecture/provider-flows.md +++ b/docs/system-architecture/provider-flows.md @@ -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 diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 081c7b5c..c400a3dc 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -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 = {}, 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 { 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 { 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 { 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 { 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 diff --git a/src/cliproxy/binary/types.ts b/src/cliproxy/binary/types.ts index c82a05cc..22e6d6fa 100644 --- a/src/cliproxy/binary/types.ts +++ b/src/cliproxy/binary/types.ts @@ -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 */ diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 8431b764..9c609e16 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -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`); } } } diff --git a/src/cliproxy/platform-detector.ts b/src/cliproxy/platform-detector.ts index 6c73c533..8a75dc4e 100644 --- a/src/cliproxy/platform-detector.ts +++ b/src/cliproxy/platform-detector.ts @@ -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'; diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts index 834df41c..c3457552 100644 --- a/src/cliproxy/services/binary-service.ts +++ b/src/cliproxy/services/binary-service.ts @@ -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 { 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 { 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); } diff --git a/src/cliproxy/services/variant-service.ts b/src/cliproxy/services/variant-service.ts index 90361c78..b5019e90 100644 --- a/src/cliproxy/services/variant-service.ts +++ b/src/cliproxy/services/variant-service.ts @@ -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 diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index 37686a50..2202e345 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -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'; diff --git a/src/commands/cliproxy/help-subcommand.ts b/src/commands/cliproxy/help-subcommand.ts index f3f81797..ccecad99 100644 --- a/src/commands/cliproxy/help-subcommand.ts +++ b/src/commands/cliproxy/help-subcommand.ts @@ -86,7 +86,7 @@ export async function showHelp(): Promise { [ [ '--backend ', - '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 ', '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 { console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.')); console.log(dim(' Routing: use gcli/ or agy/ 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')}`); diff --git a/src/web-server/services/cliproxy-dashboard-install-service.ts b/src/web-server/services/cliproxy-dashboard-install-service.ts index 305d3315..f6b9b361 100644 --- a/src/web-server/services/cliproxy-dashboard-install-service.ts +++ b/src/web-server/services/cliproxy-dashboard-install-service.ts @@ -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 { - 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); diff --git a/tests/unit/cliproxy/backend-selection.test.js b/tests/unit/cliproxy/backend-selection.test.js index 2a36c93e..8f46fb3a 100644 --- a/tests/unit/cliproxy/backend-selection.test.js +++ b/tests/unit/cliproxy/backend-selection.test.js @@ -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')); diff --git a/tests/unit/cliproxy/binary-manager-install.test.ts b/tests/unit/cliproxy/binary-manager-install.test.ts index d76f1fdd..8240c1e9 100644 --- a/tests/unit/cliproxy/binary-manager-install.test.ts +++ b/tests/unit/cliproxy/binary-manager-install.test.ts @@ -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 () => { diff --git a/tests/unit/cliproxy/variant-update-service.test.ts b/tests/unit/cliproxy/variant-update-service.test.ts index abd2039c..96df7f0e 100644 --- a/tests/unit/cliproxy/variant-update-service.test.ts +++ b/tests/unit/cliproxy/variant-update-service.test.ts @@ -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'); diff --git a/tests/unit/web-server/cliproxy-dashboard-install-service.test.ts b/tests/unit/web-server/cliproxy-dashboard-install-service.test.ts index 8c08b01e..5805ed54 100644 --- a/tests/unit/web-server/cliproxy-dashboard-install-service.test.ts +++ b/tests/unit/web-server/cliproxy-dashboard-install-service.test.ts @@ -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({ 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({ 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', }); }); }); diff --git a/tests/unit/web-server/cliproxy-stats-routes-install.test.ts b/tests/unit/web-server/cliproxy-stats-routes-install.test.ts index 296851dc..e5cfcef8 100644 --- a/tests/unit/web-server/cliproxy-stats-routes-install.test.ts +++ b/tests/unit/web-server/cliproxy-stats-routes-install.test.ts @@ -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'); });