feat(cliproxy): add user settings override for model customization

- Add getEffectiveEnvVars() to read user overrides from ~/.ccs/{provider}.settings.json
- Add ensureProviderSettings() to create settings file from defaults on first run
- Update cliproxy-executor to use effective env vars with override support
- Export new functions from index.ts

Users can now customize model mappings by editing ~/.ccs/gemini.settings.json
or ~/.ccs/codex.settings.json. This enables:
- Using gemini-3-pro-preview for all model types
- Easy migration when new models release (gemini 4, 5, etc.)
- User takes full responsibility for custom settings

Default behavior unchanged - works out of box with bundled defaults.
This commit is contained in:
kaitranntt committed 2025-11-28 19:49:28 -05:00
1 parent efb966c96d
commit 50d32ca7da
3 files changed
+90 -7

No files matched your search

+11 -6
View File
@@ -19,8 +19,9 @@ import { escapeShellArg } from '../utils/shell-executor';
import { ensureCLIProxyBinary } from './binary-manager';
import {
generateConfig,
getClaudeEnvVars,
getEffectiveEnvVars,
getProviderConfig,
ensureProviderSettings,
CLIPROXY_DEFAULT_PORT,
} from './config-generator';
import { isAuthenticated } from './auth-handler';
@@ -154,12 +155,15 @@ export async function execClaudeWithCLIProxy(
}
}
// 4. Generate config file
// 4. Ensure user settings file exists (creates from defaults if not)
ensureProviderSettings(provider);
// 5. Generate config file
log(`Generating config for ${provider}`);
const configPath = generateConfig(provider, cfg.port);
log(`Config written: ${configPath}`);
// 5. Spawn CLIProxyAPI binary
// 6. Spawn CLIProxyAPI binary
const proxyArgs = ['--config', configPath];
log(`Spawning: ${binaryPath} ${proxyArgs.join(' ')}`);
@@ -213,8 +217,9 @@ export async function execClaudeWithCLIProxy(
throw new Error(`CLIProxy startup failed: ${err.message}`);
}
// 6. Execute Claude CLI with proxied environment
const envVars = getClaudeEnvVars(provider, cfg.port);
// 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);
const env = { ...process.env, ...envVars };
log(`Claude env: ANTHROPIC_BASE_URL=${envVars.ANTHROPIC_BASE_URL}`);
@@ -244,7 +249,7 @@ export async function execClaudeWithCLIProxy(
});
}
// 7. Cleanup: kill proxy when Claude exits
// 8. Cleanup: kill proxy when Claude exits
claude.on('exit', (code, signal) => {
log(`Claude exited: code=${code}, signal=${signal}`);
proxy.kill('SIGTERM');
+76 -1
View File
@@ -10,6 +10,11 @@ import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types';
/** Settings file structure for user overrides */
interface ProviderSettings {
env: NodeJS.ProcessEnv;
}
/** Default CLIProxy port */
export const CLIPROXY_DEFAULT_PORT = 8317;
@@ -193,7 +198,15 @@ export function deleteConfig(): void {
}
/**
* Get environment variables for Claude CLI
* Get path to user settings file for provider
* Example: ~/.ccs/gemini.settings.json
*/
export function getProviderSettingsPath(provider: CLIProxyProvider): string {
return path.join(getCcsDir(), `${provider}.settings.json`);
}
/**
* Get environment variables for Claude CLI (bundled defaults)
* Uses provider-specific endpoint (e.g., /api/provider/gemini) for explicit routing.
* This enables concurrent gemini/codex usage - each session routes to its provider via URL path.
*/
@@ -213,3 +226,65 @@ export function getClaudeEnvVars(
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel || models.claudeModel,
};
}
/**
* Get effective environment variables for provider
*
* Priority order:
* 1. User settings file (~/.ccs/{provider}.settings.json) if exists
* 2. 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
): NodeJS.ProcessEnv {
const settingsPath = getProviderSettingsPath(provider);
// Check for user override file
if (fs.existsSync(settingsPath)) {
try {
const content = fs.readFileSync(settingsPath, 'utf-8');
const settings: ProviderSettings = JSON.parse(content);
if (settings.env && typeof settings.env === 'object') {
// User override found - use their settings
// Note: User is responsible for correctness
return settings.env;
}
} catch {
// Invalid JSON or structure - fall through to defaults
// Silent fallback: don't spam errors for broken user files
}
}
// No override or invalid - use bundled defaults
return getClaudeEnvVars(provider, port);
}
/**
* Copy bundled settings template to user directory if not exists
* Called during installation/first run
*/
export function ensureProviderSettings(provider: CLIProxyProvider): void {
const settingsPath = getProviderSettingsPath(provider);
// Only create if doesn't exist (preserve user edits)
if (fs.existsSync(settingsPath)) {
return;
}
// Generate default settings from PROVIDER_CONFIGS
const envVars = getClaudeEnvVars(provider);
const settings: ProviderSettings = { env: envVars };
// Ensure directory exists
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
// Write with restricted permissions
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', {
mode: 0o600,
});
}
+3
View File
@@ -46,6 +46,9 @@ export {
export {
generateConfig,
getClaudeEnvVars,
getEffectiveEnvVars,
getProviderSettingsPath,
ensureProviderSettings,
getProviderConfig,
getModelMapping,
getCliproxyDir,