mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 02:13:57 +00:00
getRemoteEnvVars was ignoring user settings and always loading
models from base config. Now follows same priority as local mode:
1. Custom settings path (variant configs)
2. User settings file (~/.ccs/{provider}.settings.json)
3. Base config defaults
This fixes the issue where selecting Opus 4.5 in UI resulted in
Gemini 3 preview being loaded when using remote proxy mode.
630 lines
21 KiB
TypeScript
630 lines
21 KiB
TypeScript
/**
|
|
* Config Generator for CLIProxyAPI
|
|
*
|
|
* Generates config.yaml for CLIProxyAPI based on provider.
|
|
* Handles OAuth token paths and provider-specific settings.
|
|
*
|
|
* Model mappings are loaded from config/base-{provider}.settings.json files
|
|
* to allow easy updates without code changes.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { getCcsDir } from '../utils/config-manager';
|
|
import { warn } from '../utils/ui';
|
|
import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types';
|
|
import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader';
|
|
import { loadOrCreateUnifiedConfig, getGlobalEnvConfig } from '../config/unified-config-loader';
|
|
|
|
/** Settings file structure for user overrides */
|
|
interface ProviderSettings {
|
|
env: NodeJS.ProcessEnv;
|
|
}
|
|
|
|
/** Default CLIProxy port */
|
|
export const CLIPROXY_DEFAULT_PORT = 8317;
|
|
|
|
/** Internal API key for CCS-managed requests */
|
|
export const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
|
|
|
|
/** Simple secret key for Control Panel login (user-facing) */
|
|
export const CCS_CONTROL_PANEL_SECRET = 'ccs';
|
|
|
|
/**
|
|
* Get CLIProxy writable directory for logs and runtime files.
|
|
* This directory is set as WRITABLE_PATH env var when spawning CLIProxy.
|
|
* Logs will be stored in ~/.ccs/cliproxy/logs/
|
|
*/
|
|
export function getCliproxyWritablePath(): string {
|
|
return path.join(getCcsDir(), 'cliproxy');
|
|
}
|
|
|
|
/**
|
|
* Config version - bump when config format changes to trigger regeneration
|
|
* v1: Initial config (port, auth-dir, api-keys only)
|
|
* v2: Full-featured config with dashboard, quota mgmt, simplified key
|
|
* v3: Logging disabled by default (user opt-in via ~/.ccs/config.yaml)
|
|
*/
|
|
export const CLIPROXY_CONFIG_VERSION = 3;
|
|
|
|
/** Provider display names (static metadata) */
|
|
const PROVIDER_DISPLAY_NAMES: Record<CLIProxyProvider, string> = {
|
|
gemini: 'Gemini',
|
|
codex: 'Codex',
|
|
agy: 'Antigravity',
|
|
qwen: 'Qwen Code',
|
|
iflow: 'iFlow',
|
|
};
|
|
|
|
/**
|
|
* Get provider configuration
|
|
* Model mappings are loaded from config/base-{provider}.settings.json
|
|
*/
|
|
export function getProviderConfig(provider: CLIProxyProvider): ProviderConfig {
|
|
const displayName = PROVIDER_DISPLAY_NAMES[provider];
|
|
if (!displayName) {
|
|
throw new Error(`Unknown provider: ${provider}`);
|
|
}
|
|
|
|
// Load models from base config file
|
|
const models = getModelMappingFromConfig(provider);
|
|
|
|
return {
|
|
name: provider,
|
|
displayName,
|
|
models,
|
|
requiresOAuth: true, // All CLIProxy providers require OAuth
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get model mapping for provider
|
|
* Loads from config/base-{provider}.settings.json
|
|
*/
|
|
export function getModelMapping(provider: CLIProxyProvider): ProviderModelMapping {
|
|
return getProviderConfig(provider).models;
|
|
}
|
|
|
|
/**
|
|
* Get CLIProxy base directory
|
|
* All CLIProxy-related files are stored under ~/.ccs/cliproxy/
|
|
*/
|
|
export function getCliproxyDir(): string {
|
|
return path.join(getCcsDir(), 'cliproxy');
|
|
}
|
|
|
|
/**
|
|
* Get auth directory for provider
|
|
* All providers use a FLAT auth directory structure for unified config.
|
|
* CLIProxyAPI stores OAuth tokens directly in auth/ (not subdirectories).
|
|
* This enables all providers to be discovered and used concurrently.
|
|
*/
|
|
export function getProviderAuthDir(_provider: CLIProxyProvider): string {
|
|
// Use flat structure - all auth files in same directory for unified discovery
|
|
// Provider param kept for API compatibility (CLIProxyAPI handles via auth file type field)
|
|
return path.join(getCliproxyDir(), 'auth');
|
|
}
|
|
|
|
/**
|
|
* Get base auth directory for CLIProxyAPI
|
|
*/
|
|
export function getAuthDir(): string {
|
|
return path.join(getCliproxyDir(), 'auth');
|
|
}
|
|
|
|
/**
|
|
* Get config file path
|
|
*/
|
|
export function getConfigPath(): string {
|
|
return path.join(getCliproxyDir(), 'config.yaml');
|
|
}
|
|
|
|
/**
|
|
* Get binary directory path
|
|
*/
|
|
export function getBinDir(): string {
|
|
return path.join(getCliproxyDir(), 'bin');
|
|
}
|
|
|
|
/**
|
|
* Get CLIProxy logging settings from user config.
|
|
* Defaults to disabled to prevent disk bloat.
|
|
*/
|
|
function getLoggingSettings(): { loggingToFile: boolean; requestLog: boolean } {
|
|
const config = loadOrCreateUnifiedConfig();
|
|
return {
|
|
loggingToFile: config.cliproxy.logging?.enabled ?? false,
|
|
requestLog: config.cliproxy.logging?.request_log ?? false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate UNIFIED config.yaml content for ALL providers
|
|
* This enables concurrent usage of gemini/codex/agy without config conflicts.
|
|
* CLIProxyAPI routes requests by model name to the appropriate provider.
|
|
*/
|
|
function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): string {
|
|
const authDir = getAuthDir(); // Base auth dir - CLIProxyAPI scans subdirectories
|
|
// Convert Windows backslashes to forward slashes for YAML compatibility
|
|
const authDirNormalized = authDir.split(path.sep).join('/');
|
|
|
|
// Get logging settings from user config (disabled by default)
|
|
const { loggingToFile, requestLog } = getLoggingSettings();
|
|
|
|
// Unified config with enhanced CLIProxyAPI features
|
|
const config = `# CLIProxyAPI config generated by CCS v${CLIPROXY_CONFIG_VERSION}
|
|
# Supports: gemini, codex, agy, qwen, iflow (concurrent usage)
|
|
# Generated: ${new Date().toISOString()}
|
|
#
|
|
# This config is auto-managed by CCS. Manual edits may be overwritten.
|
|
# Use 'ccs doctor' to regenerate with latest settings.
|
|
|
|
# =============================================================================
|
|
# Server Settings
|
|
# =============================================================================
|
|
|
|
port: ${port}
|
|
debug: false
|
|
|
|
# =============================================================================
|
|
# Logging
|
|
# =============================================================================
|
|
# WARNING: Logs can grow to several GB if enabled!
|
|
# To enable logging, edit ~/.ccs/config.yaml:
|
|
# cliproxy:
|
|
# logging:
|
|
# enabled: true
|
|
# request_log: true
|
|
# Then run 'ccs doctor --fix' to regenerate this config.
|
|
# Use 'ccs cleanup' to remove old logs.
|
|
|
|
# Write logs to file (stored in ~/.ccs/cliproxy/logs/)
|
|
logging-to-file: ${loggingToFile}
|
|
|
|
# Log individual API requests for debugging/analytics
|
|
request-log: ${requestLog}
|
|
|
|
# =============================================================================
|
|
# Dashboard & Management
|
|
# =============================================================================
|
|
|
|
# Enable usage statistics for CCS dashboard analytics
|
|
usage-statistics-enabled: true
|
|
|
|
# Remote management API for CCS dashboard integration
|
|
remote-management:
|
|
allow-remote: true
|
|
secret-key: "${CCS_CONTROL_PANEL_SECRET}"
|
|
disable-control-panel: false
|
|
|
|
# =============================================================================
|
|
# Reliability & Quota Management
|
|
# =============================================================================
|
|
|
|
# Auto-retry on transient errors (403, 408, 500, 502, 503, 504)
|
|
request-retry: 0
|
|
max-retry-interval: 0
|
|
|
|
# Auto-switch accounts on quota exceeded (429)
|
|
# This enables seamless multi-account rotation when rate limited
|
|
quota-exceeded:
|
|
switch-project: true
|
|
switch-preview-model: true
|
|
|
|
# =============================================================================
|
|
# Authentication
|
|
# =============================================================================
|
|
|
|
# API keys for CCS internal requests
|
|
api-keys:
|
|
- "${CCS_INTERNAL_API_KEY}"
|
|
|
|
# OAuth tokens directory (auto-discovered by CLIProxyAPI)
|
|
auth-dir: "${authDirNormalized}"
|
|
`;
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Generate unified config.yaml file (supports all providers concurrently)
|
|
* Only regenerates if config doesn't exist.
|
|
* @returns Path to config file
|
|
*/
|
|
export function generateConfig(
|
|
provider: CLIProxyProvider,
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): string {
|
|
const configPath = getConfigPath();
|
|
|
|
// Ensure provider auth directory exists
|
|
const authDir = getProviderAuthDir(provider);
|
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
fs.mkdirSync(authDir, { recursive: true, mode: 0o700 });
|
|
|
|
// Only generate config if it doesn't exist (unified config serves all providers)
|
|
if (!fs.existsSync(configPath)) {
|
|
const configContent = generateUnifiedConfigContent(port);
|
|
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
|
|
}
|
|
|
|
return configPath;
|
|
}
|
|
|
|
/**
|
|
* Force regenerate config.yaml with latest settings
|
|
* Deletes existing config and creates fresh one with current port
|
|
* @returns Path to new config file
|
|
*/
|
|
export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string {
|
|
const configPath = getConfigPath();
|
|
|
|
// Read existing port if config exists (preserve user's port choice)
|
|
let effectivePort = port;
|
|
if (fs.existsSync(configPath)) {
|
|
try {
|
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
const portMatch = content.match(/^port:\s*(\d+)/m);
|
|
if (portMatch) {
|
|
effectivePort = parseInt(portMatch[1], 10);
|
|
}
|
|
} catch {
|
|
// Use default port if reading fails
|
|
}
|
|
// Delete existing config
|
|
fs.unlinkSync(configPath);
|
|
}
|
|
|
|
// Ensure directories exist
|
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
fs.mkdirSync(getAuthDir(), { recursive: true, mode: 0o700 });
|
|
|
|
// Generate fresh config
|
|
const configContent = generateUnifiedConfigContent(effectivePort);
|
|
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
|
|
|
|
return configPath;
|
|
}
|
|
|
|
/**
|
|
* Check if config needs regeneration (version mismatch)
|
|
* @returns true if config should be regenerated
|
|
*/
|
|
export function configNeedsRegeneration(): boolean {
|
|
const configPath = getConfigPath();
|
|
if (!fs.existsSync(configPath)) {
|
|
return false; // Will be created on first use
|
|
}
|
|
|
|
try {
|
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
|
|
// Check for version marker
|
|
const versionMatch = content.match(/CCS v(\d+)/);
|
|
if (!versionMatch) {
|
|
return true; // No version marker = old config
|
|
}
|
|
|
|
const configVersion = parseInt(versionMatch[1], 10);
|
|
return configVersion < CLIPROXY_CONFIG_VERSION;
|
|
} catch {
|
|
return true; // Error reading = regenerate
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if config exists for provider
|
|
*/
|
|
export function configExists(): boolean {
|
|
return fs.existsSync(getConfigPath());
|
|
}
|
|
|
|
/**
|
|
* Delete config file
|
|
*/
|
|
export function deleteConfig(): void {
|
|
const configPath = getConfigPath();
|
|
if (fs.existsSync(configPath)) {
|
|
fs.unlinkSync(configPath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export function getClaudeEnvVars(
|
|
provider: CLIProxyProvider,
|
|
port: number = CLIPROXY_DEFAULT_PORT
|
|
): NodeJS.ProcessEnv {
|
|
const models = getModelMapping(provider);
|
|
|
|
// Base env vars from config file (includes ANTHROPIC_MAX_TOKENS, etc.)
|
|
const baseEnvVars = getEnvVarsFromConfig(provider);
|
|
|
|
// Core env vars that we always set dynamically
|
|
const coreEnvVars = {
|
|
// Provider-specific endpoint - routes to correct provider via URL path
|
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/api/provider/${provider}`,
|
|
ANTHROPIC_AUTH_TOKEN: CCS_INTERNAL_API_KEY,
|
|
ANTHROPIC_MODEL: models.claudeModel,
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opusModel || models.claudeModel,
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnetModel || models.claudeModel,
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel || models.claudeModel,
|
|
};
|
|
|
|
// Filter out core env vars from base config to avoid conflicts
|
|
const {
|
|
ANTHROPIC_BASE_URL: _baseUrl,
|
|
ANTHROPIC_AUTH_TOKEN: _authToken,
|
|
ANTHROPIC_MODEL: _model,
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: _opusModel,
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: _sonnetModel,
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: _haikuModel,
|
|
...additionalEnvVars
|
|
} = baseEnvVars;
|
|
|
|
// Merge core env vars with additional env vars from base config
|
|
return {
|
|
...coreEnvVars,
|
|
...additionalEnvVars, // Includes ANTHROPIC_MAX_TOKENS, etc.
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get global env vars to inject into all third-party profiles.
|
|
* Returns empty object if disabled.
|
|
*/
|
|
function getGlobalEnvVars(): Record<string, string> {
|
|
const globalEnvConfig = getGlobalEnvConfig();
|
|
if (!globalEnvConfig.enabled) {
|
|
return {};
|
|
}
|
|
return globalEnvConfig.env;
|
|
}
|
|
|
|
/** Remote proxy configuration for URL rewriting */
|
|
interface RemoteProxyRewriteConfig {
|
|
host: string;
|
|
port?: number;
|
|
protocol: 'http' | 'https';
|
|
authToken?: string;
|
|
}
|
|
|
|
/**
|
|
* Rewrite localhost URLs to remote server URLs.
|
|
* Handles various localhost patterns: 127.0.0.1, localhost, 0.0.0.0
|
|
*/
|
|
function rewriteLocalhostUrls(
|
|
envVars: NodeJS.ProcessEnv,
|
|
provider: CLIProxyProvider,
|
|
remoteConfig: RemoteProxyRewriteConfig
|
|
): NodeJS.ProcessEnv {
|
|
const result = { ...envVars };
|
|
const baseUrl = result.ANTHROPIC_BASE_URL;
|
|
|
|
if (!baseUrl) return result;
|
|
|
|
// Check if URL points to localhost (127.0.0.1, localhost, 0.0.0.0)
|
|
const localhostPattern = /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?/i;
|
|
if (!localhostPattern.test(baseUrl)) return result;
|
|
|
|
// Build remote URL with smart port handling
|
|
const defaultPort = remoteConfig.protocol === 'https' ? 443 : 80;
|
|
const effectivePort = remoteConfig.port ?? defaultPort;
|
|
const portSuffix = effectivePort === defaultPort ? '' : `:${effectivePort}`;
|
|
const remoteBaseUrl = `${remoteConfig.protocol}://${remoteConfig.host}${portSuffix}/api/provider/${provider}`;
|
|
|
|
result.ANTHROPIC_BASE_URL = remoteBaseUrl;
|
|
|
|
// Update auth token if provided
|
|
if (remoteConfig.authToken) {
|
|
result.ANTHROPIC_AUTH_TOKEN = remoteConfig.authToken;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get effective environment variables for provider
|
|
*
|
|
* Priority order:
|
|
* 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
|
|
*
|
|
* All results are merged with global_env vars (telemetry/reporting disables).
|
|
* User takes full responsibility for custom settings.
|
|
*
|
|
* If remoteRewriteConfig is provided, localhost URLs are rewritten to remote server.
|
|
*/
|
|
export function getEffectiveEnvVars(
|
|
provider: CLIProxyProvider,
|
|
port: number = CLIPROXY_DEFAULT_PORT,
|
|
customSettingsPath?: string,
|
|
remoteRewriteConfig?: RemoteProxyRewriteConfig
|
|
): NodeJS.ProcessEnv {
|
|
// Get global env vars (DISABLE_TELEMETRY, etc.)
|
|
const globalEnv = getGlobalEnvVars();
|
|
|
|
let envVars: 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 - merge with global env
|
|
envVars = { ...globalEnv, ...settings.env };
|
|
// Apply remote rewrite if configured
|
|
if (remoteRewriteConfig) {
|
|
envVars = rewriteLocalhostUrls(envVars, provider, remoteRewriteConfig);
|
|
}
|
|
return envVars;
|
|
}
|
|
} catch {
|
|
// Invalid JSON - fall through to provider defaults
|
|
console.warn(warn(`Invalid settings file: ${customSettingsPath}`));
|
|
}
|
|
} else {
|
|
console.warn(warn(`Settings file not found: ${customSettingsPath}`));
|
|
}
|
|
}
|
|
|
|
// Priority 2: Default provider settings file
|
|
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 - merge with global env
|
|
envVars = { ...globalEnv, ...settings.env };
|
|
// Apply remote rewrite if configured
|
|
if (remoteRewriteConfig) {
|
|
envVars = rewriteLocalhostUrls(envVars, provider, remoteRewriteConfig);
|
|
}
|
|
return envVars;
|
|
}
|
|
} 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 merged with global env
|
|
return { ...globalEnv, ...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,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get environment variables for remote proxy mode.
|
|
* Uses the remote proxy's provider endpoint as the base URL.
|
|
* Respects user model settings from custom settings path or provider settings file.
|
|
*
|
|
* @param provider CLIProxy provider (gemini, codex, agy, qwen, iflow)
|
|
* @param remoteConfig Remote proxy connection details
|
|
* @param customSettingsPath Optional path to user's custom settings file
|
|
* @returns Environment variables for Claude CLI
|
|
*/
|
|
export function getRemoteEnvVars(
|
|
provider: CLIProxyProvider,
|
|
remoteConfig: { host: string; port?: number; protocol: 'http' | 'https'; authToken?: string },
|
|
customSettingsPath?: string
|
|
): Record<string, string> {
|
|
// Build URL with smart port handling - omit if using protocol default
|
|
const defaultPort = remoteConfig.protocol === 'https' ? 443 : 80;
|
|
const effectivePort = remoteConfig.port ?? defaultPort;
|
|
const portSuffix = effectivePort === defaultPort ? '' : `:${effectivePort}`;
|
|
const baseUrl = `${remoteConfig.protocol}://${remoteConfig.host}${portSuffix}/api/provider/${provider}`;
|
|
|
|
// Get global env vars (DISABLE_TELEMETRY, etc.)
|
|
const globalEnv = getGlobalEnvVars();
|
|
|
|
// Load user settings with priority: custom path > user settings file > base config
|
|
let userEnvVars: Record<string, string> = {};
|
|
|
|
// 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') {
|
|
userEnvVars = settings.env as Record<string, string>;
|
|
}
|
|
} catch {
|
|
// Invalid JSON - fall through to provider defaults
|
|
console.warn(warn(`Invalid settings file: ${customSettingsPath}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Priority 2: Default provider settings file (~/.ccs/{provider}.settings.json)
|
|
if (Object.keys(userEnvVars).length === 0) {
|
|
const settingsPath = getProviderSettingsPath(provider);
|
|
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') {
|
|
userEnvVars = settings.env as Record<string, string>;
|
|
}
|
|
} catch {
|
|
// Invalid JSON - fall through to base config
|
|
}
|
|
}
|
|
}
|
|
|
|
// Priority 3: Base config defaults
|
|
if (Object.keys(userEnvVars).length === 0) {
|
|
const models = getModelMapping(provider);
|
|
const baseEnvVars = getEnvVarsFromConfig(provider);
|
|
// Filter out URL/auth from base config (we'll set those from remote config)
|
|
const {
|
|
ANTHROPIC_BASE_URL: _baseUrl,
|
|
ANTHROPIC_AUTH_TOKEN: _authToken,
|
|
...additionalEnvVars
|
|
} = baseEnvVars;
|
|
userEnvVars = {
|
|
...additionalEnvVars,
|
|
ANTHROPIC_MODEL: models.claudeModel,
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opusModel || models.claudeModel,
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnetModel || models.claudeModel,
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel || models.claudeModel,
|
|
};
|
|
}
|
|
|
|
// Build final env: global + user settings + remote URL/auth override
|
|
const env: Record<string, string> = {
|
|
...globalEnv,
|
|
...userEnvVars,
|
|
// Always override URL and auth token with remote config
|
|
ANTHROPIC_BASE_URL: baseUrl,
|
|
ANTHROPIC_AUTH_TOKEN: remoteConfig.authToken || CCS_INTERNAL_API_KEY,
|
|
};
|
|
|
|
return env;
|
|
}
|