mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(cliproxy): route plus backend to maintained fork
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user