mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
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:
1 parent
93b38d8550
commit
ce9d60d0ca
12 files changed
+129
-19
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user