feat(cliproxy): add user-defined CLIProxy variants

Allow users to create custom profile names that route through CLIProxy
providers with different model configurations.

Config example:
{
  "cliproxy": {
    "flash": { "provider": "gemini", "settings": "~/.ccs/gemini-flash.settings.json" },
    "pro": { "provider": "gemini", "settings": "~/.ccs/gemini-pro.settings.json" }
  }
}

Usage: ccs flash "quick task" or ccs pro "complex analysis"

Changes:
- Add CLIProxyVariantConfig types to src/types/config.ts
- Update profile-detector to check config.cliproxy section (Priority 1)
- Pass customSettingsPath through executor chain
- Update getEffectiveEnvVars to support custom settings path
- Document feature in README.md and CLAUDE.md
- Bump version to 5.0.2
This commit is contained in:
kaitranntt committed 2025-11-30 04:02:43 -05:00
1 parent 93b38d8550
commit ce9d60d0ca
12 files changed
+129 -19

No files matched your search

+2
View File
@@ -68,6 +68,8 @@ bun run test # Build + run all tests
### Profile Mechanisms
**CLIProxy**: gemini, codex, agy → OAuth-based, zero config
**CLIProxy Variants**: user-defined profiles in `config.cliproxy` section → custom settings for CLIProxy providers
**Settings-based**: `--settings` flag → GLM, GLMT, Kimi, default
**Account-based**: `CLAUDE_CONFIG_DIR` → isolated Claude Sub instances
+22 -3
View File
@@ -376,9 +376,10 @@ graph LR
CCS resolves profiles in priority order:
1. **CLIProxy profiles** (gemini, codex, agy) - OAuth-based, zero config
2. **Settings-based profiles** (glm, kimi) - API key required
3. **Account-based profiles** (work, personal) - Isolated Claude instances
4. **Default** - Claude CLI with subscription
2. **CLIProxy variants** (user-defined) - Custom model settings for CLIProxy providers
3. **Settings-based profiles** (glm, kimi) - API key required
4. **Account-based profiles** (work, personal) - Isolated Claude instances
5. **Default** - Claude CLI with subscription
### Profile Types
@@ -392,6 +393,24 @@ CCS resolves profiles in priority order:
- **Codex**: gpt-5.1-codex-max (opus), gpt-4o (sonnet), gpt-4o-mini (haiku)
- **Antigravity**: agy-pro (sonnet), agy-turbo (haiku)
**CLIProxy Variants** (v5.0.2+)
- User-defined profile names that route through CLIProxy providers
- Allows multiple model configurations per provider (e.g., `flash` and `pro` for Gemini)
- Configure in `~/.ccs/config.json` under `cliproxy` section:
```json
{
"profiles": { ... },
"cliproxy": {
"flash": { "provider": "gemini", "settings": "~/.ccs/gemini-flash.settings.json" },
"pro": { "provider": "gemini", "settings": "~/.ccs/gemini-pro.settings.json" },
"gpt": { "provider": "codex", "settings": "~/.ccs/codex-gpt.settings.json" }
}
}
```
Usage: `ccs flash "quick task"` or `ccs pro "complex analysis"`
**Settings-based**: GLM, GLMT, Kimi, default
- Uses `--settings` flag pointing to config files
- GLMT: Embedded proxy for thinking mode support
+1 -1
View File
@@ -1 +1 @@
5.0.1
5.0.2
+1 -1
View File
@@ -83,7 +83,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (irm | iex)
# For git installations, VERSION file is read if available
$CcsVersion = "5.0.1"
$CcsVersion = "5.0.2"
# Try to read VERSION file for git installations
if ($ScriptDir) {
+1 -1
View File
@@ -84,7 +84,7 @@ fi
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (curl | bash)
# For git installations, VERSION file is read if available
CCS_VERSION="5.0.1"
CCS_VERSION="5.0.2"
# Try to read VERSION file for git installations
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "5.0.1",
"version": "5.0.2",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
+43 -5
View File
@@ -23,6 +23,8 @@ export interface ProfileDetectionResult {
settingsPath?: string;
profile?: Settings | ProfileMetadata;
message?: string;
/** For cliproxy variants: the underlying provider (gemini, codex, agy) */
provider?: CLIProxyProfileName;
}
export interface AllProfiles {
@@ -85,6 +87,12 @@ class ProfileDetector {
/**
* Detect profile type and return routing information
*
* Priority order:
* 0. Hardcoded CLIProxy profiles (gemini, codex, agy)
* 1. User-defined CLIProxy variants (config.cliproxy section)
* 2. Settings-based profiles (config.profiles section)
* 3. Account-based profiles (profiles.json)
*/
detectProfileType(profileName: string | null | undefined): ProfileDetectionResult {
// Special case: 'default' means use default profile
@@ -97,12 +105,24 @@ class ProfileDetector {
return {
type: 'cliproxy',
name: profileName,
provider: profileName as CLIProxyProfileName,
};
}
// Priority 1: Check settings-based profiles (glm, kimi) - BACKWARD COMPATIBILITY
// Priority 1: Check user-defined CLIProxy variants (config.cliproxy section)
const config = this.readConfig();
if (config.cliproxy && config.cliproxy[profileName]) {
const variant = config.cliproxy[profileName];
return {
type: 'cliproxy',
name: profileName,
provider: variant.provider as CLIProxyProfileName,
settingsPath: variant.settings,
};
}
// Priority 2: Check settings-based profiles (glm, kimi) - BACKWARD COMPATIBILITY
if (config.profiles && config.profiles[profileName]) {
return {
type: 'settings',
@@ -111,7 +131,7 @@ class ProfileDetector {
};
}
// Priority 2: Check account-based profiles (work, personal)
// Priority 3: Check account-based profiles (work, personal)
const profiles = this.readProfiles();
if (profiles.profiles && profiles.profiles[profileName]) {
@@ -124,7 +144,12 @@ class ProfileDetector {
// Not found - generate suggestions
const allProfiles = this.getAllProfiles();
const allProfileNames = [...allProfiles.settings, ...allProfiles.accounts];
const allProfileNames = [
...allProfiles.cliproxy,
...allProfiles.cliproxyVariants,
...allProfiles.settings,
...allProfiles.accounts,
];
const suggestions = findSimilarStrings(profileName, allProfileNames);
const error = new Error(`Profile not found: ${profileName}`) as ProfileNotFoundError;
@@ -180,8 +205,20 @@ class ProfileDetector {
lines.push(` - ${name}`);
});
// Settings-based profiles
// CLIProxy variants (user-defined)
const config = this.readConfig();
const cliproxyVariants = Object.keys(config.cliproxy || {});
const cliproxyConfig = config.cliproxy;
if (cliproxyVariants.length > 0 && cliproxyConfig) {
lines.push('CLIProxy variants (user-defined):');
cliproxyVariants.forEach((name) => {
const variant = cliproxyConfig[name];
lines.push(` - ${name} (${variant.provider})`);
});
}
// Settings-based profiles
const settingsProfiles = Object.keys(config.profiles || {});
if (settingsProfiles.length > 0) {
@@ -221,7 +258,7 @@ class ProfileDetector {
/**
* Get all available profile names
*/
getAllProfiles(): AllProfiles & { cliproxy: string[] } {
getAllProfiles(): AllProfiles & { cliproxy: string[]; cliproxyVariants: string[] } {
const config = this.readConfig();
const profiles = this.readProfiles();
@@ -229,6 +266,7 @@ class ProfileDetector {
settings: Object.keys(config.profiles || {}),
accounts: Object.keys(profiles.profiles || {}),
cliproxy: [...CLIPROXY_PROFILES],
cliproxyVariants: Object.keys(config.cliproxy || {}),
default: profiles.default,
};
}
+4 -2
View File
@@ -295,8 +295,10 @@ async function main(): Promise<void> {
const profileInfo = detector.detectProfileType(profile);
if (profileInfo.type === 'cliproxy') {
// CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy)
await execClaudeWithCLIProxy(claudeCli, profileInfo.name as CLIProxyProvider, remainingArgs);
// CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy) or user-defined variants
const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider);
const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles
await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, { customSettingsPath });
} else if (profileInfo.type === 'settings') {
// Check if this is GLMT profile (requires proxy)
if (profileInfo.name === 'glmt') {
+2 -2
View File
@@ -218,8 +218,8 @@ export async function execClaudeWithCLIProxy(
}
// 7. Execute Claude CLI with proxied environment
// Uses user settings if ~/.ccs/{provider}.settings.json exists, else bundled defaults
const envVars = getEffectiveEnvVars(provider, cfg.port);
// Uses custom settings path (for variants), user settings, or bundled defaults
const envVars = getEffectiveEnvVars(provider, cfg.port, cfg.customSettingsPath);
const env = { ...process.env, ...envVars };
log(`Claude env: ANTHROPIC_BASE_URL=${envVars.ANTHROPIC_BASE_URL}`);
+27 -3
View File
@@ -211,16 +211,40 @@ export function getClaudeEnvVars(
* Get effective environment variables for provider
*
* Priority order:
* 1. User settings file (~/.ccs/{provider}.settings.json) if exists
* 2. Bundled defaults from PROVIDER_CONFIGS
* 1. Custom settings path (for user-defined CLIProxy variants)
* 2. User settings file (~/.ccs/{provider}.settings.json) if exists
* 3. Bundled defaults from PROVIDER_CONFIGS
*
* This allows users to customize model mappings without code changes.
* User takes full responsibility for custom settings.
*/
export function getEffectiveEnvVars(
provider: CLIProxyProvider,
port: number = CLIPROXY_DEFAULT_PORT
port: number = CLIPROXY_DEFAULT_PORT,
customSettingsPath?: string
): NodeJS.ProcessEnv {
// Priority 1: Custom settings path (for user-defined variants)
if (customSettingsPath) {
const expandedPath = customSettingsPath.replace(/^~/, require('os').homedir());
if (fs.existsSync(expandedPath)) {
try {
const content = fs.readFileSync(expandedPath, 'utf-8');
const settings: ProviderSettings = JSON.parse(content);
if (settings.env && typeof settings.env === 'object') {
// Custom variant settings found - use them
return settings.env;
}
} catch {
// Invalid JSON - fall through to provider defaults
console.warn(`[!] Warning: Invalid settings file: ${customSettingsPath}`);
}
} else {
console.warn(`[!] Warning: Settings file not found: ${customSettingsPath}`);
}
}
// Priority 2: Default provider settings file
const settingsPath = getProviderSettingsPath(provider);
// Check for user override file
+2
View File
@@ -150,6 +150,8 @@ export interface ExecutorConfig {
verbose: boolean;
/** Poll interval for port check in ms (default: 100) */
pollInterval: number;
/** Custom settings path for user-defined CLIProxy variants */
customSettingsPath?: string;
}
/**
+23
View File
@@ -11,12 +11,35 @@ export interface ProfilesConfig {
[profileName: string]: string; // Path to settings.json
}
/**
* CLIProxy variant configuration
* Allows user-defined CLIProxy profile variants with custom settings
* Example: "flash" → gemini provider with gemini-2.5-flash model
*/
export interface CLIProxyVariantConfig {
/** CLIProxy provider to use (gemini, codex, agy) */
provider: 'gemini' | 'codex' | 'agy';
/** Path to settings.json with custom model configuration */
settings: string;
}
/**
* CLIProxy variants section in config.json
* Maps custom profile names to CLIProxy provider + settings
*/
export interface CLIProxyVariantsConfig {
[profileName: string]: CLIProxyVariantConfig;
}
/**
* Main CCS configuration
* Located at: ~/.ccs/config.json
*/
export interface Config {
/** Settings-based profiles (GLM, Kimi, etc.) */
profiles: ProfilesConfig;
/** User-defined CLIProxy profile variants (optional) */
cliproxy?: CLIProxyVariantsConfig;
}
/**