mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
562 lines
20 KiB
TypeScript
562 lines
20 KiB
TypeScript
/**
|
|
* Environment variable builder for CLIProxy
|
|
* Handles env var construction, merging, and remote URL rewriting
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import { CLIProxyProvider, ProviderModelMapping } from '../types';
|
|
import { getModelMappingFromConfig, getEnvVarsFromConfig } from '../base-config-loader';
|
|
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
|
|
import { getEffectiveApiKey } from '../auth-token-manager';
|
|
import { expandPath } from '../../utils/helpers';
|
|
import { warn } from '../../utils/ui';
|
|
import { CompositeTierConfig } from '../../config/unified-config-types';
|
|
import {
|
|
validatePort,
|
|
validateRemotePort,
|
|
getRemoteDefaultPort,
|
|
normalizeProtocol,
|
|
CLIPROXY_DEFAULT_PORT,
|
|
} from './port-manager';
|
|
import { getProviderSettingsPath } from './path-resolver';
|
|
|
|
/** Settings file structure for user overrides */
|
|
interface ProviderSettings {
|
|
env: NodeJS.ProcessEnv;
|
|
}
|
|
|
|
/** Model name prefix that was deprecated in CLIProxyAPI registry */
|
|
const DEPRECATED_MODEL_PREFIX = 'gemini-claude-';
|
|
/** Replacement prefix matching actual upstream model names */
|
|
const UPSTREAM_MODEL_PREFIX = 'claude-';
|
|
|
|
/** Env vars that contain model names and may need migration */
|
|
const MODEL_ENV_KEYS = [
|
|
'ANTHROPIC_MODEL',
|
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
];
|
|
|
|
/**
|
|
* Migrate deprecated gemini-claude-* model names to upstream claude-* names in a settings file.
|
|
* CLIProxyAPI registry no longer recognizes the gemini-claude-* prefix convention.
|
|
* Preserves any suffixes like (high), [1m], etc.
|
|
*
|
|
* Returns true if migration was performed and file was updated.
|
|
*/
|
|
function migrateDeprecatedModelNames(settingsPath: string, settings: ProviderSettings): boolean {
|
|
if (!settings.env || typeof settings.env !== 'object') return false;
|
|
|
|
let migrated = false;
|
|
for (const key of MODEL_ENV_KEYS) {
|
|
const value = settings.env[key];
|
|
if (typeof value !== 'string') continue;
|
|
|
|
// Check if the base model name (before any suffixes) uses the deprecated prefix
|
|
if (value.toLowerCase().startsWith(DEPRECATED_MODEL_PREFIX)) {
|
|
settings.env[key] = UPSTREAM_MODEL_PREFIX + value.slice(DEPRECATED_MODEL_PREFIX.length);
|
|
migrated = true;
|
|
}
|
|
}
|
|
|
|
if (migrated) {
|
|
try {
|
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
|
|
} catch {
|
|
// Best-effort migration — don't block startup if write fails
|
|
}
|
|
}
|
|
|
|
return migrated;
|
|
}
|
|
|
|
/** Remote proxy configuration for URL rewriting */
|
|
export interface RemoteProxyRewriteConfig {
|
|
host: string;
|
|
port?: number;
|
|
protocol: 'http' | 'https';
|
|
authToken?: string;
|
|
}
|
|
|
|
/**
|
|
* Get model mapping for provider
|
|
* Loads from config/base-{provider}.settings.json
|
|
*/
|
|
export function getModelMapping(provider: CLIProxyProvider): ProviderModelMapping {
|
|
return getModelMappingFromConfig(provider);
|
|
}
|
|
|
|
/**
|
|
* 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: getEffectiveApiKey(),
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Ensure required CLIProxy env vars are present.
|
|
* Falls back to bundled defaults if missing from user settings.
|
|
* This prevents 404 errors when users forget to set BASE_URL/AUTH_TOKEN.
|
|
*/
|
|
function ensureRequiredEnvVars(
|
|
envVars: NodeJS.ProcessEnv,
|
|
provider: CLIProxyProvider,
|
|
port: number
|
|
): NodeJS.ProcessEnv {
|
|
const validPort = validatePort(port);
|
|
const result = { ...envVars };
|
|
const defaults = getClaudeEnvVars(provider, validPort);
|
|
|
|
// Fill in missing required vars from defaults
|
|
if (!result.ANTHROPIC_BASE_URL?.trim()) {
|
|
result.ANTHROPIC_BASE_URL = defaults.ANTHROPIC_BASE_URL;
|
|
}
|
|
if (!result.ANTHROPIC_AUTH_TOKEN?.trim()) {
|
|
result.ANTHROPIC_AUTH_TOKEN = defaults.ANTHROPIC_AUTH_TOKEN;
|
|
}
|
|
|
|
// Normalize local CLIProxy root/wrong-provider URLs to provider-pinned endpoint.
|
|
// This prevents model-routed "unknown provider" failures for codex effort aliases.
|
|
if (result.ANTHROPIC_BASE_URL?.trim()) {
|
|
result.ANTHROPIC_BASE_URL = normalizeLocalProviderBaseUrl(
|
|
result.ANTHROPIC_BASE_URL,
|
|
provider,
|
|
validPort
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/** Localhost hostnames used for local CLIProxy endpoints */
|
|
const LOCALHOST_NAMES = new Set(['127.0.0.1', 'localhost', '0.0.0.0']);
|
|
|
|
/**
|
|
* Normalize local CLIProxy endpoint to the expected provider route.
|
|
* Only rewrites localhost URLs that target the active local port.
|
|
*/
|
|
function normalizeLocalProviderBaseUrl(
|
|
baseUrl: string,
|
|
provider: CLIProxyProvider,
|
|
port: number
|
|
): string {
|
|
try {
|
|
const parsed = new URL(baseUrl);
|
|
if (!['http:', 'https:'].includes(parsed.protocol)) return baseUrl;
|
|
if (!LOCALHOST_NAMES.has(parsed.hostname.toLowerCase())) return baseUrl;
|
|
|
|
const effectivePort = parsed.port
|
|
? Number.parseInt(parsed.port, 10)
|
|
: parsed.protocol === 'https:'
|
|
? 443
|
|
: 80;
|
|
if (!Number.isFinite(effectivePort) || effectivePort !== port) return baseUrl;
|
|
|
|
const expectedPath = `/api/provider/${provider}`;
|
|
if (parsed.pathname === expectedPath && !parsed.search && !parsed.hash) return baseUrl;
|
|
|
|
parsed.pathname = expectedPath;
|
|
parsed.search = '';
|
|
parsed.hash = '';
|
|
return parsed.toString();
|
|
} catch {
|
|
return baseUrl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 (8317 for HTTP, 443 for HTTPS)
|
|
// Validate port and normalize protocol for defensive handling
|
|
const normalizedProtocol = normalizeProtocol(remoteConfig.protocol);
|
|
const validatedPort = validateRemotePort(remoteConfig.port);
|
|
const effectivePort = validatedPort ?? getRemoteDefaultPort(normalizedProtocol);
|
|
// Omit port suffix for standard web ports (80/443) for cleaner URLs
|
|
const standardWebPort = normalizedProtocol === 'https' ? 443 : 80;
|
|
const portSuffix = effectivePort === standardWebPort ? '' : `:${effectivePort}`;
|
|
const remoteBaseUrl = `${normalizedProtocol}://${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 = expandPath(customSettingsPath);
|
|
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') {
|
|
// Migrate deprecated gemini-claude-* model names if present
|
|
migrateDeprecatedModelNames(expandedPath, settings);
|
|
// Custom variant settings found - merge with global env
|
|
envVars = { ...globalEnv, ...settings.env };
|
|
// Ensure required vars are present (fall back to defaults if missing)
|
|
envVars = ensureRequiredEnvVars(envVars, provider, port);
|
|
// 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') {
|
|
// Migrate deprecated gemini-claude-* model names if present
|
|
migrateDeprecatedModelNames(settingsPath, settings);
|
|
// User override found - merge with global env
|
|
envVars = { ...globalEnv, ...settings.env };
|
|
// Ensure required vars are present (fall back to defaults if missing)
|
|
envVars = ensureRequiredEnvVars(envVars, provider, port);
|
|
// 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(require('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 (8317 for HTTP, 443 for HTTPS)
|
|
// Validate port and normalize protocol for defensive handling
|
|
const normalizedProtocol = normalizeProtocol(remoteConfig.protocol);
|
|
const validatedPort = validateRemotePort(remoteConfig.port);
|
|
const effectivePort = validatedPort ?? getRemoteDefaultPort(normalizedProtocol);
|
|
// Omit port suffix for standard web ports (80/443) for cleaner URLs
|
|
const standardWebPort = normalizedProtocol === 'https' ? 443 : 80;
|
|
const portSuffix = effectivePort === standardWebPort ? '' : `:${effectivePort}`;
|
|
// Remote CLIProxyAPI uses root path (e.g., /v1/messages), not /api/provider/{provider}/v1/messages
|
|
// The /api/provider/ prefix is only for local CLIProxy instances
|
|
const baseUrl = `${normalizedProtocol}://${remoteConfig.host}${portSuffix}`;
|
|
|
|
// 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 = expandPath(customSettingsPath);
|
|
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') {
|
|
migrateDeprecatedModelNames(expandedPath, settings);
|
|
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') {
|
|
migrateDeprecatedModelNames(settingsPath, settings);
|
|
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 || getEffectiveApiKey(),
|
|
};
|
|
|
|
return env;
|
|
}
|
|
|
|
/** Remote config for composite variant (passed from env-resolver) */
|
|
export interface CompositeRemoteConfig {
|
|
host: string;
|
|
port: number;
|
|
protocol: 'http' | 'https';
|
|
authToken?: string;
|
|
}
|
|
|
|
/**
|
|
* Get environment variables for composite variant.
|
|
* Uses root URL (no /api/provider/ path) for model-based routing.
|
|
* Each tier maps to a different provider's model, routed by CLIProxyAPI.
|
|
*
|
|
* @param tiers Per-tier provider+model mappings
|
|
* @param defaultTier Which tier ANTHROPIC_MODEL equals
|
|
* @param port Local CLIProxy port (ignored if remoteConfig provided)
|
|
* @param customSettingsPath Optional path to user's custom settings file
|
|
* @param remoteConfig Optional remote proxy config (overrides localhost URL/auth)
|
|
*/
|
|
export function getCompositeEnvVars(
|
|
tiers: { opus: CompositeTierConfig; sonnet: CompositeTierConfig; haiku: CompositeTierConfig },
|
|
defaultTier: 'opus' | 'sonnet' | 'haiku',
|
|
port: number = CLIPROXY_DEFAULT_PORT,
|
|
customSettingsPath?: string,
|
|
remoteConfig?: CompositeRemoteConfig
|
|
): Record<string, string> {
|
|
const globalEnv = getGlobalEnvVars();
|
|
|
|
// Load user settings if provided (may contain additional env vars like hooks)
|
|
let additionalEnvVars: Record<string, string> = {};
|
|
if (customSettingsPath) {
|
|
const expandedPath = expandPath(customSettingsPath);
|
|
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') {
|
|
// Extract non-core env vars (hooks, etc.)
|
|
const {
|
|
ANTHROPIC_BASE_URL: _baseUrl,
|
|
ANTHROPIC_AUTH_TOKEN: _authToken,
|
|
ANTHROPIC_MODEL: _model,
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: _opus,
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: _sonnet,
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: _haiku,
|
|
...extra
|
|
} = settings.env as Record<string, string>;
|
|
additionalEnvVars = extra;
|
|
}
|
|
} catch {
|
|
// Invalid JSON — ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
const validPort = validatePort(port);
|
|
|
|
// Defensive: handle missing tiers gracefully
|
|
const opusModel = tiers.opus?.model;
|
|
const sonnetModel = tiers.sonnet?.model;
|
|
const haikuModel = tiers.haiku?.model;
|
|
const defaultModel = tiers[defaultTier]?.model;
|
|
|
|
// If default tier is missing, we cannot proceed meaningfully
|
|
if (!defaultModel) {
|
|
throw new Error(`Missing model for default tier '${defaultTier}'`);
|
|
}
|
|
|
|
// Determine base URL and auth token based on remote vs local mode
|
|
const baseUrl = remoteConfig
|
|
? (() => {
|
|
const normalizedProtocol = normalizeProtocol(remoteConfig.protocol);
|
|
const effectivePort =
|
|
validateRemotePort(remoteConfig.port) ?? getRemoteDefaultPort(normalizedProtocol);
|
|
const standardWebPort = normalizedProtocol === 'https' ? 443 : 80;
|
|
const portSuffix = effectivePort === standardWebPort ? '' : `:${effectivePort}`;
|
|
return `${normalizedProtocol}://${remoteConfig.host}${portSuffix}`;
|
|
})()
|
|
: `http://127.0.0.1:${validPort}`;
|
|
|
|
const authToken = remoteConfig?.authToken ?? getEffectiveApiKey();
|
|
|
|
const env: Record<string, string> = {
|
|
...globalEnv,
|
|
...additionalEnvVars,
|
|
// Root URL — CLIProxyAPI routes based on model name in request body
|
|
ANTHROPIC_BASE_URL: baseUrl,
|
|
ANTHROPIC_AUTH_TOKEN: authToken,
|
|
ANTHROPIC_MODEL: defaultModel,
|
|
};
|
|
|
|
// Only set tier env vars if the tier exists
|
|
if (opusModel) env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
|
if (sonnetModel) env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel;
|
|
if (haikuModel) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel;
|
|
|
|
return env;
|
|
}
|