mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-08 20:19:37 +00:00
Merge pull request #1064 from kaitranntt/kai/hotfix/1062-cliproxy-plus-deleted
hotfix(cliproxy): fallback from deleted CLIProxyAPIPlus to original backend
This commit is contained in:
+115
-12
@@ -5,10 +5,17 @@
|
||||
* 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 } from './platform-detector';
|
||||
import {
|
||||
BACKEND_CONFIG,
|
||||
DEFAULT_BACKEND,
|
||||
CLIPROXY_MAX_STABLE_VERSION,
|
||||
getExecutableName,
|
||||
} from './platform-detector';
|
||||
import { stopProxy } from './services/proxy-lifecycle-service';
|
||||
import { waitForPortFree } from '../utils/port-utils';
|
||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||
@@ -16,6 +23,7 @@ import {
|
||||
UpdateCheckResult,
|
||||
checkForUpdates,
|
||||
deleteBinary,
|
||||
getVersionCachePath,
|
||||
getBinaryPath,
|
||||
isBinaryInstalled,
|
||||
getBinaryInfo,
|
||||
@@ -30,11 +38,72 @@ 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';
|
||||
|
||||
/**
|
||||
* Get backend from config or default to 'plus'
|
||||
* Track whether we've already warned the user about the Plus fallback this
|
||||
* process lifetime. Prevents spamming the warning on every command.
|
||||
*/
|
||||
function getConfiguredBackend(): CLIProxyBackend {
|
||||
let plusFallbackWarned = false;
|
||||
|
||||
function emitPlusFallbackWarning(): void {
|
||||
if (plusFallbackWarned) return;
|
||||
plusFallbackWarned = true;
|
||||
process.stderr.write(
|
||||
`${warn(
|
||||
'CLIProxyAPIPlus upstream repo is currently unavailable; local CLIProxy is falling back to ' +
|
||||
'`backend: original`. Run `ccs config` to update your saved config. ' +
|
||||
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlusBackendUnavailableMessage(provider?: string): string {
|
||||
const prefix = provider
|
||||
? `${provider} requires CLIProxyAPIPlus,`
|
||||
: 'CLIProxyAPIPlus upstream repo is currently unavailable,';
|
||||
return (
|
||||
`${prefix} but local CLIProxy currently supports only \`backend: original\`. ` +
|
||||
`Tracking: ${CLIPROXY_PLUS_TRACKING_URL}`
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveLocalBackend(
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND,
|
||||
options: { warnOnFallback?: boolean } = {}
|
||||
): CLIProxyBackend {
|
||||
if (backend !== 'plus') return backend;
|
||||
if (options.warnOnFallback) {
|
||||
emitPlusFallbackWarning();
|
||||
}
|
||||
return 'original';
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return config.cliproxy?.backend || DEFAULT_BACKEND;
|
||||
@@ -43,6 +112,24 @@ function getConfiguredBackend(): CLIProxyBackend {
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoredConfiguredBackend(): CLIProxyBackend {
|
||||
return getConfiguredOrDefaultBackend();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend {
|
||||
return resolveLocalBackend(getConfiguredOrDefaultBackend(), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backend-specific binary directory.
|
||||
* Stores binaries in separate dirs: bin/original/ and bin/plus/
|
||||
@@ -52,7 +139,7 @@ function getBackendBinDir(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
return `${baseDir}/${backend}`;
|
||||
}
|
||||
|
||||
/** Default configuration (uses backend from config.yaml or defaults to 'plus') */
|
||||
/** Default configuration (uses backend from config.yaml or defaults to `DEFAULT_BACKEND`) */
|
||||
function createDefaultConfig(backend: CLIProxyBackend = DEFAULT_BACKEND): BinaryManagerConfig {
|
||||
const backendConfig = BACKEND_CONFIG[backend];
|
||||
return {
|
||||
@@ -76,7 +163,9 @@ export class BinaryManager {
|
||||
private backend: CLIProxyBackend;
|
||||
|
||||
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
|
||||
this.backend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
this.backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
const defaultConfig = createDefaultConfig(this.backend);
|
||||
this.config = { ...defaultConfig, ...config };
|
||||
}
|
||||
@@ -127,7 +216,9 @@ export async function ensureCLIProxyBinary(
|
||||
verbose = false,
|
||||
options: EnsureCLIProxyBinaryOptions = {}
|
||||
): Promise<string> {
|
||||
const backend = getConfiguredBackend();
|
||||
const configuredBackend = getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
|
||||
// Migrate old shared pin to backend-specific location (one-time migration)
|
||||
migrateVersionPin(backend);
|
||||
@@ -158,19 +249,25 @@ export async function ensureCLIProxyBinary(
|
||||
|
||||
/** Check if CLIProxyAPI binary is installed */
|
||||
export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
return new BinaryManager({}, effectiveBackend).isBinaryInstalled();
|
||||
}
|
||||
|
||||
/** Get CLIProxyAPI binary path (may not exist) */
|
||||
export function getCLIProxyPath(backend?: CLIProxyBackend): string {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
return new BinaryManager({}, effectiveBackend).getBinaryPath();
|
||||
}
|
||||
|
||||
/** Get installed CLIProxyAPI version from .version file */
|
||||
export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
return readInstalledVersion(
|
||||
getBackendBinDir(effectiveBackend),
|
||||
BACKEND_CONFIG[effectiveBackend].fallbackVersion
|
||||
@@ -196,7 +293,9 @@ export async function installCliproxyVersion(
|
||||
backend?: CLIProxyBackend,
|
||||
deps: InstallCliproxyVersionDeps = {}
|
||||
): Promise<void> {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
const manager =
|
||||
deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ??
|
||||
new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
|
||||
@@ -237,7 +336,9 @@ export async function installCliproxyVersion(
|
||||
|
||||
/** Fetch the latest CLIProxyAPI version from GitHub API */
|
||||
export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
|
||||
return result.latestVersion;
|
||||
}
|
||||
@@ -262,7 +363,9 @@ export interface CliproxyUpdateCheckResult {
|
||||
export async function checkCliproxyUpdate(
|
||||
backend?: CLIProxyBackend
|
||||
): Promise<CliproxyUpdateCheckResult> {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
|
||||
|
||||
// Import isNewerVersion for stability check
|
||||
|
||||
@@ -18,7 +18,11 @@ import { ProgressIndicator } from '../../utils/progress-indicator';
|
||||
import { ok, fail, info, warn } from '../../utils/ui';
|
||||
import { getCcsDir } from '../../utils/config-manager';
|
||||
import { escapeShellArg, getWindowsEscapedCommandShell } from '../../utils/shell-executor';
|
||||
import { ensureCLIProxyBinary } from '../binary-manager';
|
||||
import {
|
||||
ensureCLIProxyBinary,
|
||||
getConfiguredBackend,
|
||||
getPlusBackendUnavailableMessage,
|
||||
} from '../binary-manager';
|
||||
import {
|
||||
generateConfig,
|
||||
getProviderConfig,
|
||||
@@ -30,7 +34,6 @@ import {
|
||||
import { checkRemoteProxy } from '../remote-proxy-client';
|
||||
import { isAuthenticated } from '../auth-handler';
|
||||
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types';
|
||||
import { DEFAULT_BACKEND } from '../platform-detector';
|
||||
import { configureProviderModel, getCurrentModel } from '../model-config';
|
||||
import { reconcileCodexModelForActivePlan } from '../codex-plan-compatibility';
|
||||
import { resolveProxyConfig, PROXY_CLI_FLAGS } from '../proxy-config-resolver';
|
||||
@@ -207,23 +210,8 @@ export async function execClaudeWithCLIProxy(
|
||||
// 0. Resolve proxy configuration (CLI > ENV > config.yaml > defaults)
|
||||
const unifiedConfig = loadOrCreateUnifiedConfig();
|
||||
|
||||
// 0a. Runtime backend/provider validation
|
||||
const backend: CLIProxyBackend = unifiedConfig.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
|
||||
// Collect all providers to validate (default + composite tiers)
|
||||
const allProviders = [provider, ...compositeProviders];
|
||||
for (const p of allProviders) {
|
||||
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
|
||||
console.error('');
|
||||
console.error(fail(`${p} requires CLIProxyAPIPlus backend`));
|
||||
console.error('');
|
||||
console.error('To use this provider, either:');
|
||||
console.error(' 1. Set `cliproxy.backend: plus` in ~/.ccs/config.yaml');
|
||||
console.error(' 2. Use --backend=plus flag: ccs ' + p + ' --backend=plus');
|
||||
console.error('');
|
||||
throw new Error(`Provider ${p} requires Plus backend`);
|
||||
}
|
||||
}
|
||||
|
||||
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
|
||||
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
|
||||
@@ -283,6 +271,7 @@ export async function execClaudeWithCLIProxy(
|
||||
|
||||
// Check remote proxy if configured
|
||||
let useRemoteProxy = false;
|
||||
let localBackend: CLIProxyBackend = 'original';
|
||||
if (proxyConfig.mode === 'remote' && proxyConfig.host) {
|
||||
const status = await checkRemoteProxy({
|
||||
host: proxyConfig.host,
|
||||
@@ -330,6 +319,19 @@ export async function execClaudeWithCLIProxy(
|
||||
}
|
||||
}
|
||||
|
||||
if (!useRemoteProxy) {
|
||||
localBackend = getConfiguredBackend({ warnOnFallback: true });
|
||||
|
||||
for (const p of allProviders) {
|
||||
if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
|
||||
console.error('');
|
||||
console.error(fail(getPlusBackendUnavailableMessage(p)));
|
||||
console.error('');
|
||||
throw new Error(`Provider ${p} is temporarily unavailable on local CLIProxy`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Variables for local proxy mode
|
||||
let binaryPath: string | undefined;
|
||||
let sessionId: string | undefined;
|
||||
@@ -969,13 +971,13 @@ export async function execClaudeWithCLIProxy(
|
||||
cfg.port,
|
||||
cfg.timeout,
|
||||
cfg.pollInterval,
|
||||
backend,
|
||||
localBackend,
|
||||
configPath
|
||||
);
|
||||
|
||||
// Register session
|
||||
if (proxy.pid) {
|
||||
sessionId = registerProxySession(cfg.port, proxy.pid, backend, verbose);
|
||||
sessionId = registerProxySession(cfg.port, proxy.pid, localBackend, verbose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,15 @@ export const BACKEND_CONFIG = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** Default backend */
|
||||
export const DEFAULT_BACKEND: CLIProxyBackend = 'plus';
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
export const DEFAULT_BACKEND: CLIProxyBackend = 'original';
|
||||
|
||||
/**
|
||||
* CLIProxyAPIPlus fallback version (used when GitHub API unavailable)
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
savePinnedVersion,
|
||||
clearPinnedVersion,
|
||||
isVersionPinned,
|
||||
resolveLocalBackend,
|
||||
syncPlusFallbackStateIfNeeded,
|
||||
} from '../binary-manager';
|
||||
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
|
||||
import { CLIProxyBackend } from '../types';
|
||||
@@ -54,8 +56,10 @@ export interface LatestVersionResult {
|
||||
* Get current binary status for a specific backend
|
||||
*/
|
||||
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
const backendConfig = BACKEND_CONFIG[effectiveBackend];
|
||||
return {
|
||||
installed: isCLIProxyInstalled(effectiveBackend),
|
||||
@@ -71,8 +75,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
|
||||
* Check for latest version
|
||||
*/
|
||||
export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> {
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
|
||||
try {
|
||||
// Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo)
|
||||
@@ -119,8 +125,10 @@ export async function installVersion(
|
||||
};
|
||||
}
|
||||
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
|
||||
try {
|
||||
await installCliproxyVersion(version, verbose, effectiveBackend);
|
||||
@@ -147,8 +155,10 @@ export async function installLatest(
|
||||
verbose = false,
|
||||
backend?: CLIProxyBackend
|
||||
): Promise<InstallResult> {
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
|
||||
try {
|
||||
const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend);
|
||||
@@ -184,8 +194,10 @@ export async function installLatest(
|
||||
* Check if a version is pinned
|
||||
*/
|
||||
export function isPinned(backend?: CLIProxyBackend): boolean {
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
return isVersionPinned(effectiveBackend);
|
||||
}
|
||||
|
||||
@@ -193,8 +205,10 @@ export function isPinned(backend?: CLIProxyBackend): boolean {
|
||||
* Get pinned version if any
|
||||
*/
|
||||
export function getPinned(backend?: CLIProxyBackend): string | null {
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
return getPinnedVersion(effectiveBackend);
|
||||
}
|
||||
|
||||
@@ -202,7 +216,9 @@ export function getPinned(backend?: CLIProxyBackend): string | null {
|
||||
* Clear version pin
|
||||
*/
|
||||
export function clearPin(backend?: CLIProxyBackend): void {
|
||||
const effectiveBackend =
|
||||
const configuredBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
syncPlusFallbackStateIfNeeded(configuredBackend);
|
||||
const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
|
||||
clearPinnedVersion(effectiveBackend);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CLIProxyProfileName } from '../../auth/profile-detector';
|
||||
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS } from '../types';
|
||||
import { CLIProxyProvider, PLUS_ONLY_PROVIDERS } from '../types';
|
||||
import { CompositeTierConfig, CompositeVariantConfig } from '../../config/unified-config-types';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { DEFAULT_BACKEND } from '../platform-detector';
|
||||
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||
import { deleteConfigForPort } from '../config-generator';
|
||||
import { hasActiveSessions, deleteSessionLockForPort } from '../session-tracker';
|
||||
@@ -44,6 +42,7 @@ import {
|
||||
removeVariantFromLegacyConfig,
|
||||
getNextAvailablePort,
|
||||
} from './variant-config-adapter';
|
||||
import { getConfiguredBackend, getPlusBackendUnavailableMessage } from '../binary-manager';
|
||||
|
||||
// Re-export VariantConfig from adapter
|
||||
export type { VariantConfig } from './variant-config-adapter';
|
||||
@@ -80,16 +79,15 @@ export function validateProfileName(name: string): string | null {
|
||||
|
||||
/**
|
||||
* Validate provider/backend compatibility
|
||||
* Returns error message if provider requires Plus backend but original is configured
|
||||
* Returns error message if a provider requires Plus while local CLIProxy is
|
||||
* running with the fallbacked original backend.
|
||||
*/
|
||||
export function validateProviderBackend(provider: CLIProxyProfileName): string | null {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
const backend: CLIProxyBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
|
||||
// Normalize provider to lowercase for case-insensitive comparison
|
||||
const normalizedProvider = provider.toLowerCase() as CLIProxyProvider;
|
||||
const backend = getConfiguredBackend();
|
||||
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(normalizedProvider)) {
|
||||
return `${provider} requires CLIProxyAPIPlus. Set \`cliproxy.backend: plus\` in config.yaml or use --backend=plus`;
|
||||
return getPlusBackendUnavailableMessage(provider);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -280,6 +278,13 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.provider !== undefined) {
|
||||
const backendError = validateProviderBackend(updates.provider);
|
||||
if (backendError) {
|
||||
return { success: false, error: backendError };
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings file
|
||||
if (existing.settings) {
|
||||
const settingsPath = existing.settings.replace(/^~/, os.homedir());
|
||||
@@ -302,14 +307,6 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
||||
// Update config entry if provider/account/target changed
|
||||
if (updates.provider !== undefined || updates.account !== undefined || targetChanged) {
|
||||
const newProvider = updates.provider ?? existing.provider;
|
||||
|
||||
// Validate provider/backend compatibility on provider change
|
||||
if (updates.provider !== undefined) {
|
||||
const backendError = validateProviderBackend(updates.provider);
|
||||
if (backendError) {
|
||||
return { success: false, error: backendError };
|
||||
}
|
||||
}
|
||||
const newAccount = updates.account !== undefined ? updates.account : existing.account;
|
||||
const newTarget = updates.target ?? existingTarget;
|
||||
|
||||
|
||||
@@ -148,8 +148,10 @@ export type CLIProxyProvider =
|
||||
|
||||
/**
|
||||
* CLIProxy backend selection
|
||||
* - original: CLIProxyAPI (legacy provider subset)
|
||||
* - plus: CLIProxyAPIPlus (expanded provider support, default)
|
||||
* - 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.)
|
||||
*/
|
||||
export type CLIProxyBackend = 'original' | 'plus';
|
||||
|
||||
|
||||
@@ -84,7 +84,10 @@ export async function showHelp(): Promise<void> {
|
||||
[
|
||||
'Options:',
|
||||
[
|
||||
['--backend <type>', 'Use specific backend: original | plus (default: from config)'],
|
||||
[
|
||||
'--backend <type>',
|
||||
'Use specific backend: original | plus (local default: original; plus currently falls back locally)',
|
||||
],
|
||||
['--target <cli>', 'Default target for created/edited variants: claude | droid'],
|
||||
['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'],
|
||||
],
|
||||
@@ -102,6 +105,11 @@ 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('');
|
||||
console.log(subheader('Notes:'));
|
||||
console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`);
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
*/
|
||||
|
||||
import { CLIProxyBackend } from '../../cliproxy/types';
|
||||
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
||||
import { getStoredConfiguredBackend } from '../../cliproxy/binary-manager';
|
||||
import {
|
||||
type QuotaSupportedProvider,
|
||||
QUOTA_PROVIDER_HELP_TEXT,
|
||||
mapExternalProviderName,
|
||||
isQuotaSupportedProvider,
|
||||
} from '../../cliproxy/provider-capabilities';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { handleSync } from '../cliproxy-sync-handler';
|
||||
import { extractOption, hasAnyFlag } from '../arg-extractor';
|
||||
|
||||
@@ -71,12 +70,10 @@ function parseBackendArg(args: string[]): {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective backend (CLI flag > config.yaml > default)
|
||||
* Get selected backend input (CLI flag > config.yaml > default)
|
||||
*/
|
||||
function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend {
|
||||
if (cliBackend) return cliBackend;
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
return cliBackend ?? getStoredConfiguredBackend();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function showStatus(verbose: boolean, backend: CLIProxyBackend): Pr
|
||||
console.log(` ${dim('Run "ccs gemini" or any provider to auto-install')}`);
|
||||
}
|
||||
|
||||
const latestCheck = await checkLatestVersion();
|
||||
const latestCheck = await checkLatestVersion(backend);
|
||||
if (latestCheck.success && latestCheck.latestVersion) {
|
||||
console.log('');
|
||||
if (latestCheck.updateAvailable) {
|
||||
|
||||
@@ -402,7 +402,7 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
||||
backend:
|
||||
partial.cliproxy?.backend === 'original' || partial.cliproxy?.backend === 'plus'
|
||||
? partial.cliproxy.backend
|
||||
: undefined, // Invalid values become undefined (defaults to 'plus' at runtime)
|
||||
: undefined, // Invalid values become undefined (defaults to 'original' at runtime)
|
||||
// Auto-sync - default to true
|
||||
auto_sync: partial.cliproxy?.auto_sync ?? defaults.cliproxy.auto_sync ?? true,
|
||||
routing: {
|
||||
|
||||
@@ -210,7 +210,7 @@ export interface CLIProxyRoutingConfig {
|
||||
* CLIProxy configuration section.
|
||||
*/
|
||||
export interface CLIProxyConfig {
|
||||
/** Backend selection: 'original' or 'plus' (default: 'plus') */
|
||||
/** Backend selection: 'original' or 'plus' (default: 'original') */
|
||||
backend?: 'original' | 'plus';
|
||||
/** Nickname to email mapping for OAuth accounts */
|
||||
oauth_accounts: OAuthAccounts;
|
||||
@@ -1001,7 +1001,7 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
||||
accounts: {},
|
||||
profiles: {},
|
||||
cliproxy: {
|
||||
backend: 'plus',
|
||||
backend: 'original',
|
||||
oauth_accounts: {},
|
||||
providers: [...CLIPROXY_SUPPORTED_PROVIDERS],
|
||||
variants: {},
|
||||
|
||||
@@ -38,7 +38,11 @@ import {
|
||||
} from '../../cliproxy/config-generator';
|
||||
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker';
|
||||
import { ensureCliproxyService } from '../../cliproxy/service-manager';
|
||||
import { checkCliproxyUpdate, getInstalledCliproxyVersion } from '../../cliproxy/binary-manager';
|
||||
import {
|
||||
checkCliproxyUpdate,
|
||||
getInstalledCliproxyVersion,
|
||||
getStoredConfiguredBackend,
|
||||
} from '../../cliproxy/binary-manager';
|
||||
import {
|
||||
fetchAllVersions,
|
||||
isNewerVersion,
|
||||
@@ -47,9 +51,7 @@ import {
|
||||
import {
|
||||
CLIPROXY_MAX_STABLE_VERSION,
|
||||
CLIPROXY_FAULTY_RANGE,
|
||||
DEFAULT_BACKEND,
|
||||
} from '../../cliproxy/platform-detector';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/port-manager';
|
||||
import {
|
||||
MODEL_ENV_VAR_KEYS,
|
||||
@@ -143,18 +145,8 @@ export function shouldCacheQuotaResult(result: {
|
||||
return !transientPatterns.some((p) => msg.includes(p));
|
||||
}
|
||||
|
||||
/** Get configured backend from config */
|
||||
function getConfiguredBackend() {
|
||||
try {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return config.cliproxy?.backend || DEFAULT_BACKEND;
|
||||
} catch {
|
||||
return DEFAULT_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUpdateCheckFallback(
|
||||
backend: ReturnType<typeof getConfiguredBackend>,
|
||||
backend: ReturnType<typeof getStoredConfiguredBackend>,
|
||||
getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion
|
||||
) {
|
||||
const currentVersion = getInstalledVersionFn(backend);
|
||||
@@ -178,7 +170,7 @@ function buildUpdateCheckFallback(
|
||||
}
|
||||
|
||||
function buildVersionsFallback(
|
||||
backend: ReturnType<typeof getConfiguredBackend>,
|
||||
backend: ReturnType<typeof getStoredConfiguredBackend>,
|
||||
getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion
|
||||
) {
|
||||
const currentVersion = getInstalledVersionFn(backend);
|
||||
@@ -206,7 +198,7 @@ interface ResolveVersionsDeps {
|
||||
}
|
||||
|
||||
export async function resolveCliproxyUpdateCheckPayload(
|
||||
backend: ReturnType<typeof getConfiguredBackend>,
|
||||
backend: ReturnType<typeof getStoredConfiguredBackend>,
|
||||
deps: ResolveUpdateCheckDeps = {}
|
||||
) {
|
||||
const checkCliproxyUpdateFn = deps.checkCliproxyUpdateFn ?? checkCliproxyUpdate;
|
||||
@@ -218,7 +210,7 @@ export async function resolveCliproxyUpdateCheckPayload(
|
||||
}
|
||||
|
||||
export async function resolveCliproxyVersionsPayload(
|
||||
backend: ReturnType<typeof getConfiguredBackend>,
|
||||
backend: ReturnType<typeof getStoredConfiguredBackend>,
|
||||
deps: ResolveVersionsDeps = {}
|
||||
) {
|
||||
const fetchAllVersionsFn = deps.fetchAllVersionsFn ?? fetchAllVersions;
|
||||
@@ -409,7 +401,7 @@ router.post('/proxy-stop', async (_req: Request, res: Response): Promise<void> =
|
||||
*/
|
||||
router.get('/update-check', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const backend = getConfiguredBackend();
|
||||
const backend = getStoredConfiguredBackend();
|
||||
const result = await resolveCliproxyUpdateCheckPayload(backend);
|
||||
|
||||
res.json(result);
|
||||
@@ -1019,7 +1011,7 @@ router.get('/quota/:provider/:accountId', async (req: Request, res: Response): P
|
||||
*/
|
||||
router.get('/versions', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const backend = getConfiguredBackend();
|
||||
const backend = getStoredConfiguredBackend();
|
||||
res.json(await resolveCliproxyVersionsPayload(backend));
|
||||
} catch (error) {
|
||||
console.error(`[cliproxy-stats] ${(error as Error).message}`);
|
||||
@@ -1073,7 +1065,7 @@ router.post('/install', async (req: Request, res: Response): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const backend = getConfiguredBackend();
|
||||
const backend = getStoredConfiguredBackend();
|
||||
const installResult = await installDashboardCliproxyVersion(version, backend);
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { installCliproxyVersion } from '../../cliproxy/binary-manager';
|
||||
import {
|
||||
installCliproxyVersion,
|
||||
resolveLocalBackend,
|
||||
syncPlusFallbackStateIfNeeded,
|
||||
} from '../../cliproxy/binary-manager';
|
||||
import { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/service-manager';
|
||||
import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker';
|
||||
import { isCliproxyRunning } from '../../cliproxy/stats-fetcher';
|
||||
@@ -52,12 +56,14 @@ export async function installDashboardCliproxyVersion(
|
||||
backend: CLIProxyBackend,
|
||||
deps: InstallDashboardCliproxyVersionDeps = defaultDeps
|
||||
): Promise<DashboardCliproxyInstallResult> {
|
||||
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||
syncPlusFallbackStateIfNeeded(backend);
|
||||
const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true });
|
||||
const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||
const shouldRestoreService = await wasProxyRunning(deps);
|
||||
|
||||
// The installer owns the stop-and-replace lifecycle, including best-effort
|
||||
// shutdown for tracked and untracked proxies before swapping the binary.
|
||||
await deps.installCliproxyVersion(version, true, backend);
|
||||
await deps.installCliproxyVersion(version, true, effectiveBackend);
|
||||
|
||||
if (!shouldRestoreService) {
|
||||
return {
|
||||
|
||||
@@ -28,8 +28,8 @@ describe('Backend Selection', () => {
|
||||
});
|
||||
|
||||
describe('DEFAULT_BACKEND', () => {
|
||||
it('defaults to plus backend for backward compatibility', () => {
|
||||
assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'plus');
|
||||
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => {
|
||||
assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'original');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,9 +45,10 @@ describe('Backend Selection', () => {
|
||||
assert(info.binaryName.startsWith('CLIProxyAPIPlus_6.6.51-0_'));
|
||||
});
|
||||
|
||||
it('uses plus backend by default', () => {
|
||||
it('uses original backend by default (Plus upstream deleted, issue #1062)', () => {
|
||||
const info = platformDetector.detectPlatform();
|
||||
assert(info.binaryName.includes('CLIProxyAPIPlus'));
|
||||
assert(info.binaryName.startsWith('CLIProxyAPI_'));
|
||||
assert(!info.binaryName.includes('CLIProxyAPIPlus'));
|
||||
});
|
||||
|
||||
it('uses fallback version when version not specified', () => {
|
||||
@@ -76,9 +77,10 @@ describe('Backend Selection', () => {
|
||||
assert.strictEqual(name, expected);
|
||||
});
|
||||
|
||||
it('defaults to plus backend', () => {
|
||||
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => {
|
||||
const name = platformDetector.getExecutableName();
|
||||
assert(name.includes('cli-proxy-api-plus'));
|
||||
const expected = isWindows ? 'cli-proxy-api.exe' : 'cli-proxy-api';
|
||||
assert.strictEqual(name, expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,9 +96,10 @@ describe('Backend Selection', () => {
|
||||
assert(url.includes('router-for-me/CLIProxyAPIPlus/releases'));
|
||||
});
|
||||
|
||||
it('defaults to plus backend', () => {
|
||||
it('defaults to original backend (Plus upstream deleted, issue #1062)', () => {
|
||||
const url = platformDetector.getDownloadUrl();
|
||||
assert(url.includes('CLIProxyAPIPlus'));
|
||||
assert(url.includes('router-for-me/CLIProxyAPI/releases'));
|
||||
assert(!url.includes('CLIProxyAPIPlus'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,80 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('installCliproxyVersion', () => {
|
||||
it('degrades explicit plus backend requests to original before install flows run', async () => {
|
||||
let seenBackend: string | undefined;
|
||||
|
||||
const binaryManager = await import(
|
||||
`../../../src/cliproxy/binary-manager?binary-manager-explicit-plus=${Date.now()}`
|
||||
);
|
||||
|
||||
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus', {
|
||||
createManager: (_config: unknown, backend: string) => {
|
||||
seenBackend = backend;
|
||||
return {
|
||||
isBinaryInstalled: () => false,
|
||||
deleteBinary: () => undefined,
|
||||
ensureBinary: async () => '/tmp/ccs-bin/original/cliproxy',
|
||||
};
|
||||
},
|
||||
stopProxyFn: async () => ({ stopped: false, error: 'No active CLIProxy session found' }),
|
||||
waitForPortFreeFn: async () => true,
|
||||
formatInfo: (message: string) => message,
|
||||
formatWarn: (message: string) => message,
|
||||
getInstalledVersion: () => '6.6.80',
|
||||
});
|
||||
|
||||
expect(seenBackend).toBe('original');
|
||||
});
|
||||
|
||||
it('returns original and emits a real warning when plus backend is resolved locally', async () => {
|
||||
const binaryManager = await import(
|
||||
`../../../src/cliproxy/binary-manager?binary-manager-warning=${Date.now()}`
|
||||
);
|
||||
|
||||
const writes: string[] = [];
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
writes.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
|
||||
try {
|
||||
expect(binaryManager.resolveLocalBackend('plus', { warnOnFallback: true })).toBe('original');
|
||||
} finally {
|
||||
process.stderr.write = originalWrite;
|
||||
}
|
||||
|
||||
expect(writes.join('')).toContain('CLIProxyAPIPlus upstream repo is currently unavailable');
|
||||
expect(writes.join('')).toContain('backend: original');
|
||||
});
|
||||
|
||||
it('reuses plus binary and pin state when local runtime falls back to original', 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');
|
||||
const { getExecutableName } = await import('../../../src/cliproxy/platform-detector');
|
||||
const binaryService = await import(
|
||||
`../../../src/cliproxy/services/binary-service?binary-service-plus-migration=${Date.now()}`
|
||||
);
|
||||
|
||||
const config = createEmptyUnifiedConfig();
|
||||
config.cliproxy = { ...config.cliproxy, backend: 'plus' };
|
||||
saveUnifiedConfig(config);
|
||||
|
||||
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
|
||||
fs.mkdirSync(plusBinDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(plusBinDir, getExecutableName('plus')), 'fake-binary');
|
||||
fs.writeFileSync(path.join(plusBinDir, '.version'), '6.6.80-0');
|
||||
savePinnedVersion('6.6.80-0', 'plus');
|
||||
|
||||
const status = binaryService.getBinaryStatus();
|
||||
|
||||
expect(status.installed).toBe(true);
|
||||
expect(status.pinnedVersion).toBe('6.6.80-0');
|
||||
expect(status.binaryPath).toContain('/original/');
|
||||
});
|
||||
|
||||
it('attempts to stop the proxy even when there is no tracked running session', async () => {
|
||||
const calls = {
|
||||
stopProxy: 0,
|
||||
@@ -78,7 +152,7 @@ describe('installCliproxyVersion', () => {
|
||||
skipAutoUpdate: true,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'CLIProxy Plus binary is not installed locally. Run "ccs cliproxy install" when you have network access.'
|
||||
'CLIProxy binary is not installed locally. Run "ccs cliproxy install" when you have network access.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,10 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { updateVariant } from '../../../src/cliproxy/services/variant-service';
|
||||
import {
|
||||
updateVariant,
|
||||
validateProviderBackend,
|
||||
} from '../../../src/cliproxy/services/variant-service';
|
||||
import { loadOrCreateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||
|
||||
describe('updateVariant - provider/model consistency', () => {
|
||||
@@ -50,6 +53,7 @@ preferences:
|
||||
telemetry: false
|
||||
auto_update: true
|
||||
cliproxy:
|
||||
backend: plus
|
||||
oauth_accounts: {}
|
||||
providers:
|
||||
- gemini
|
||||
@@ -92,6 +96,25 @@ cliproxy:
|
||||
expect(result.error).toContain('denylist');
|
||||
});
|
||||
|
||||
it('reports plus-only providers as temporarily unavailable on local CLIProxy', () => {
|
||||
const error = validateProviderBackend('ghcp');
|
||||
expect(error).toContain('currently supports only `backend: original`');
|
||||
expect(error).toContain('issues/1062');
|
||||
});
|
||||
|
||||
it('leaves the settings file unchanged when a plus-only provider update is rejected', () => {
|
||||
const settingsPath = path.join(tmpDir, 'gemini-demo.settings.json');
|
||||
const before = fs.readFileSync(settingsPath, 'utf-8');
|
||||
|
||||
const result = updateVariant('demo', {
|
||||
provider: 'ghcp',
|
||||
model: 'gpt-5.4-mini',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(fs.readFileSync(settingsPath, 'utf-8')).toBe(before);
|
||||
});
|
||||
|
||||
it('updates provider and regenerates provider-specific core env in same settings file', () => {
|
||||
const result = updateVariant('demo', {
|
||||
provider: 'codex',
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('installDashboardCliproxyVersion', () => {
|
||||
success: true,
|
||||
restarted: true,
|
||||
port: 8317,
|
||||
message: 'Successfully installed CLIProxy Plus v6.7.1 and restarted it on port 8317',
|
||||
message: 'Successfully installed CLIProxy v6.7.1 and restarted it on port 8317',
|
||||
});
|
||||
expect(calls.isCliproxyRunning).toBe(0);
|
||||
expect(calls.installCliproxyVersion).toBe(1);
|
||||
@@ -71,7 +71,7 @@ describe('installDashboardCliproxyVersion', () => {
|
||||
expect(result).toEqual<DashboardCliproxyInstallResult>({
|
||||
success: true,
|
||||
restarted: false,
|
||||
message: 'Successfully installed CLIProxy Plus v6.7.1',
|
||||
message: 'Successfully installed CLIProxy v6.7.1',
|
||||
});
|
||||
expect(calls.isCliproxyRunning).toBe(1);
|
||||
expect(calls.installCliproxyVersion).toBe(1);
|
||||
@@ -118,8 +118,8 @@ describe('installDashboardCliproxyVersion', () => {
|
||||
expect(result).toEqual<DashboardCliproxyInstallResult>({
|
||||
success: false,
|
||||
restarted: false,
|
||||
error: 'Installed CLIProxy Plus v6.7.1, but restart failed',
|
||||
message: 'Installed CLIProxy Plus v6.7.1, but failed to restart it',
|
||||
error: 'Installed CLIProxy v6.7.1, but restart failed',
|
||||
message: 'Installed CLIProxy v6.7.1, but failed to restart it',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ let createEmptyUnifiedConfig: typeof import('../../../src/config/unified-config-
|
||||
let saveUnifiedConfig: typeof import('../../../src/config/unified-config-loader').saveUnifiedConfig;
|
||||
let setGlobalConfigDir: typeof import('../../../src/utils/config-manager').setGlobalConfigDir;
|
||||
let writeInstalledVersion: typeof import('../../../src/cliproxy/binary/version-cache').writeInstalledVersion;
|
||||
let writeVersionCache: typeof import('../../../src/cliproxy/binary/version-cache').writeVersionCache;
|
||||
let writeVersionListCache: typeof import('../../../src/cliproxy/binary/version-cache').writeVersionListCache;
|
||||
|
||||
let server: Server;
|
||||
@@ -25,7 +26,7 @@ beforeAll(async () => {
|
||||
({ setGlobalConfigDir } = await import('../../../src/utils/config-manager'));
|
||||
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
|
||||
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
|
||||
({ writeInstalledVersion, writeVersionListCache } = await import(
|
||||
({ writeInstalledVersion, writeVersionCache, writeVersionListCache } = await import(
|
||||
'../../../src/cliproxy/binary/version-cache'
|
||||
));
|
||||
|
||||
@@ -39,6 +40,7 @@ beforeAll(async () => {
|
||||
saveUnifiedConfig(config);
|
||||
|
||||
writeInstalledVersion(plusBinDir, '6.6.80');
|
||||
writeVersionCache('6.6.89', 'plus');
|
||||
writeVersionListCache(
|
||||
{
|
||||
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
|
||||
@@ -94,6 +96,23 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
describe('cliproxy-stats-routes install contract', () => {
|
||||
it('routes saved plus configs through original backend for update checks', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/cliproxy/update-check`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const body = (await response.json()) as {
|
||||
backend: string;
|
||||
backendLabel: string;
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
};
|
||||
|
||||
expect(body.backend).toBe('original');
|
||||
expect(body.backendLabel).toBe('CLIProxy');
|
||||
expect(body.currentVersion).toBe('6.6.80');
|
||||
expect(body.latestVersion).toBe('6.6.89');
|
||||
});
|
||||
|
||||
it('returns faultyRange in the versions response', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/cliproxy/versions`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
@@ -36,7 +36,7 @@ import { useTranslation } from 'react-i18next';
|
||||
const DEBUG_MODE_KEY = 'ccs_debug_mode';
|
||||
|
||||
/** Providers only available on CLIProxyAPIPlus */
|
||||
const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp'];
|
||||
const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp', 'cursor', 'gitlab', 'codebuddy', 'kilo'];
|
||||
|
||||
function normalizeRiskAckPhrase(value: string): string {
|
||||
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
||||
@@ -191,7 +191,7 @@ export default function ProxySection() {
|
||||
}, [isAgyConfirmPhraseValid, persistAgyAckBypass, t]);
|
||||
|
||||
// Backend state (loaded from API) + mutation hook for proper query invalidation
|
||||
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
|
||||
const [backend, setBackend] = useState<'original' | 'plus'>('original');
|
||||
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
|
||||
const updateBackendMutation = useUpdateBackend();
|
||||
const { data: proxyStatus } = useProxyStatus();
|
||||
@@ -490,9 +490,6 @@ export default function ProxySection() {
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-medium">{t('settingsProxy.backendPlusApi')}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400">
|
||||
{t('settingsProxy.default')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('settingsProxy.plusDesc')}</p>
|
||||
</button>
|
||||
@@ -509,10 +506,22 @@ export default function ProxySection() {
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-medium">{t('settingsProxy.backendApi')}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400">
|
||||
{t('settingsProxy.default')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('settingsProxy.originalDesc')}</p>
|
||||
</button>
|
||||
</div>
|
||||
{backend === 'plus' && (
|
||||
<Alert className="py-2 border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20 [&>svg]:top-2.5">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
<AlertDescription className="text-amber-700 dark:text-amber-400">
|
||||
CLIProxyAPIPlus upstream is currently unavailable. Local CLIProxy will use the
|
||||
original backend until issue #1062 is resolved.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{/* Warning when original backend selected with Kiro/ghcp variants */}
|
||||
{backend === 'original' && hasKiroGhcpVariants && (
|
||||
<Alert variant="destructive" className="py-2">
|
||||
|
||||
Reference in New Issue
Block a user