UNPKG

@kdump/code-cli-any-llm

Version:

> A unified gateway for the Gemini, opencode, crush, and Qwen Code AI CLIs

718 lines (713 loc) 29.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GlobalConfigService = void 0; const common_1 = require("@nestjs/common"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const yaml = __importStar(require("js-yaml")); const DEFAULT_GATEWAY_LOG_DIR = path.join(os.homedir(), '.code-cli-any-llm', 'logs'); const parseBetaEnv = (value) => { if (!value) { return undefined; } const normalized = value.trim(); if (!normalized) { return undefined; } return normalized .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0); }; const normalizeBoolean = (value, defaultValue = true) => { if (value === undefined) { return defaultValue; } const normalized = value.trim().toLowerCase(); if (!normalized) { return defaultValue; } if (['false', '0', 'no', 'off'].includes(normalized)) { return false; } if (['true', '1', 'yes', 'on'].includes(normalized)) { return true; } return defaultValue; }; let GlobalConfigService = class GlobalConfigService { configDir; configFile; constructor() { this.configDir = path.join(os.homedir(), '.code-cli-any-llm'); this.configFile = path.join(this.configDir, 'config.yaml'); } loadGlobalConfig() { try { let mergedConfig = {}; const configSources = []; const envAiProviderRaw = (process.env.CAL_AI_PROVIDER || 'openai').toLowerCase(); const envAiProvider = envAiProviderRaw === 'codex' ? 'codex' : envAiProviderRaw === 'claudecode' ? 'claudeCode' : 'openai'; const envConfig = { aiProvider: envAiProvider, openai: { apiKey: process.env.CAL_OPENAI_API_KEY || '', baseURL: process.env.CAL_OPENAI_BASE_URL || 'https://open.bigmodel.cn/api/paas/v4', model: process.env.CAL_OPENAI_MODEL || 'glm-4.6', timeout: Number(process.env.CAL_OPENAI_TIMEOUT) || 1800000, extraBody: undefined, }, codex: process.env.CAL_CODEX_API_KEY ? { apiKey: process.env.CAL_CODEX_API_KEY || '', baseURL: process.env.CAL_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex', model: process.env.CAL_CODEX_MODEL || 'gpt-5-codex', timeout: Number(process.env.CAL_CODEX_TIMEOUT) || 1800000, reasoning: (() => { const raw = process.env.CAL_CODEX_REASONING; if (!raw) return undefined; try { return JSON.parse(raw); } catch { return undefined; } })(), textVerbosity: (() => { const raw = (process.env.CAL_CODEX_TEXT_VERBOSITY || '').toLowerCase(); return ['low', 'medium', 'high'].includes(raw) ? raw : undefined; })(), } : undefined, claudeCode: (() => { const apiKey = process.env.CAL_CLAUDE_CODE_API_KEY || process.env.CAL_ANTHROPIC_API_KEY || ''; if (!apiKey.trim()) { return undefined; } return { apiKey: apiKey.trim(), baseURL: process.env.CAL_CLAUDE_CODE_BASE_URL || 'https://open.bigmodel.cn/api/anthropic', model: process.env.CAL_CLAUDE_CODE_MODEL || 'claude-sonnet-4-5-20250929', timeout: Number(process.env.CAL_CLAUDE_CODE_TIMEOUT) || 1800000, anthropicVersion: process.env.CAL_CLAUDE_CODE_VERSION || '2023-06-01', beta: parseBetaEnv(process.env.CAL_CLAUDE_CODE_BETA), userAgent: process.env.CAL_CLAUDE_CODE_USER_AGENT || 'claude-cli/2.0.1 (external, cli)', xApp: process.env.CAL_CLAUDE_CODE_X_APP || 'cli', dangerousDirectBrowserAccess: normalizeBoolean(process.env.CAL_CLAUDE_CODE_DANGEROUS_DIRECT, true), maxOutputTokens: process.env.CAL_CLAUDE_CODE_MAX_OUTPUT ? Number(process.env.CAL_CLAUDE_CODE_MAX_OUTPUT) : undefined, extraHeaders: undefined, }; })(), gateway: { port: Number(process.env.CAL_PORT) || 23062, host: process.env.CAL_HOST || '0.0.0.0', logLevel: process.env.CAL_LOG_LEVEL || 'info', logDir: process.env.CAL_GATEWAY_LOG_DIR || DEFAULT_GATEWAY_LOG_DIR, requestTimeout: Number(process.env.CAL_REQUEST_TIMEOUT) || 3600000, }, }; mergedConfig = this.deepMerge(mergedConfig, envConfig); if (process.env.CAL_OPENAI_API_KEY || process.env.CAL_OPENAI_BASE_URL || process.env.CAL_OPENAI_MODEL || process.env.CAL_AI_PROVIDER || process.env.CAL_CODEX_API_KEY || process.env.CAL_CODEX_BASE_URL || process.env.CAL_CODEX_MODEL || process.env.CAL_CODEX_REASONING || process.env.CAL_CODEX_TEXT_VERBOSITY || process.env.CAL_CLAUDE_CODE_API_KEY || process.env.CAL_CLAUDE_CODE_BASE_URL || process.env.CAL_CLAUDE_CODE_MODEL || process.env.CAL_CLAUDE_CODE_VERSION || process.env.CAL_CLAUDE_CODE_BETA || process.env.CAL_CLAUDE_CODE_USER_AGENT || process.env.CAL_CLAUDE_CODE_X_APP || process.env.CAL_CLAUDE_CODE_DANGEROUS_DIRECT || process.env.CAL_CLAUDE_CODE_MAX_OUTPUT || process.env.CAL_PORT || process.env.CAL_HOST || process.env.CAL_LOG_LEVEL || process.env.CAL_GATEWAY_LOG_DIR || process.env.CAL_REQUEST_TIMEOUT) { configSources.push('Environment variables'); } if (!fs.existsSync(this.configFile)) { this.createConfigTemplate(); } const globalConfigContent = fs.readFileSync(this.configFile, 'utf8'); const globalConfig = yaml.load(globalConfigContent); if (globalConfig) { mergedConfig = this.deepMerge(mergedConfig, globalConfig); configSources.push(this.configFile); } const projectConfigFile = path.join(process.cwd(), 'config', 'config.yaml'); if (fs.existsSync(projectConfigFile)) { const projectConfigContent = fs.readFileSync(projectConfigFile, 'utf8'); const projectConfig = yaml.load(projectConfigContent); if (projectConfig) { mergedConfig = this.deepMerge(mergedConfig, projectConfig); configSources.unshift(projectConfigFile); } } const result = this.validateConfig(mergedConfig); if (result.config) { let primarySource = 'Default configuration'; if (configSources.length > 0) { const fileSource = configSources.find((source) => source.includes('.yaml')); primarySource = fileSource || configSources[0]; } result.config.configSource = primarySource; result.config.configSources = configSources; } return result; } catch (error) { return { isValid: false, errors: [ { field: 'config', message: `Failed to load configuration file: ${error.message}`, suggestion: 'Please verify that the configuration file format is valid.', required: true, }, ], warnings: [], }; } } saveConfig(config) { if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 }); } const content = yaml.dump(config, { indent: 2, lineWidth: 120 }); fs.writeFileSync(this.configFile, content, { mode: 0o600 }); } deepMerge(target, source) { if (!source) { return target ?? {}; } const result = { ...(target ?? {}) }; for (const [key, value] of Object.entries(source)) { if (value !== null && typeof value === 'object' && !Array.isArray(value)) { const existing = result[key]; result[key] = this.deepMerge(existing ?? {}, value); } else if (value !== '' || result[key] === undefined) { result[key] = value; } } return result; } createConfigTemplate() { if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 }); } const template = this.getDefaultTemplate(); fs.writeFileSync(this.configFile, template.template, { mode: 0o600 }); } getDefaultTemplate() { const template = `# Global configuration for code-cli-any-llm # Edit this file to configure your default API settings aiProvider: openai # OpenAI-compatible provider (default) openai: apiKey: "" baseURL: "https://open.bigmodel.cn/api/paas/v4" model: "glm-4.6" timeout: 1800000 # Codex provider (optional) codex: apiKey: "" baseURL: "https://chatgpt.com/backend-api/codex" model: "gpt-5-codex" timeout: 1800000 reasoning: effort: minimal summary: auto textVerbosity: low # Claude Code provider (optional) claudeCode: apiKey: "" baseURL: "https://open.bigmodel.cn/api/anthropic" model: "claude-sonnet-4-5-20250929" timeout: 1800000 anthropicVersion: "2023-06-01" beta: - claude-code-20250219 userAgent: "claude-cli/2.0.1 (external, cli)" xApp: "cli" dangerousDirectBrowserAccess: true maxOutputTokens: 64000 # Gateway Configuration gateway: port: 23062 host: "0.0.0.0" logLevel: "info" logDir: "~/.code-cli-any-llm/logs" requestTimeout: 3600000 `; return { template, comments: true, }; } validateConfig(rawConfig) { const config = JSON.parse(JSON.stringify(rawConfig ?? {})); const errors = []; const warnings = []; let openaiConfig = config.openai; const gatewayCliModeRaw = (config.gateway?.cliMode || 'gemini') .toString() .trim() .toLowerCase(); if (gatewayCliModeRaw === 'opencode' || gatewayCliModeRaw === 'crush' || gatewayCliModeRaw === 'qwencode') { config.gateway = config.gateway ?? {}; config.gateway.apiMode = 'openai'; } const requireOpenAIKey = (config.aiProvider ?? 'openai') === 'openai'; if (!openaiConfig) { const defaultConfig = { apiKey: '', baseURL: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4.6', timeout: 1800000, extraBody: undefined, }; if (requireOpenAIKey) { errors.push({ field: 'openai', message: 'OpenAI configuration is missing', suggestion: 'Add an openai configuration section.', required: true, }); } openaiConfig = defaultConfig; config.openai = openaiConfig; } else { const trimmedApiKey = openaiConfig.apiKey?.trim(); if (!trimmedApiKey && requireOpenAIKey) { errors.push({ field: 'openai.apiKey', message: 'API key is empty', suggestion: 'Set a valid API key in the configuration file.', required: true, }); } openaiConfig.apiKey = trimmedApiKey ?? ''; if (!openaiConfig.baseURL) { warnings.push('baseURL is not set; using the default value.'); openaiConfig.baseURL = 'https://open.bigmodel.cn/api/paas/v4'; } if (!openaiConfig.model) { warnings.push('model is not set; using the default value.'); openaiConfig.model = 'glm-4.6'; } if (!openaiConfig.timeout) { warnings.push('timeout is not set; using the default value.'); openaiConfig.timeout = 1800000; } } const aiProviderRaw = (config.aiProvider || 'claudeCode') .toString() .toLowerCase(); let aiProvider; if (aiProviderRaw === 'codex') { aiProvider = 'codex'; } else if (aiProviderRaw === 'claudecode') { aiProvider = 'claudeCode'; } else if (aiProviderRaw === 'openai') { aiProvider = 'openai'; } else { errors.push({ field: 'aiProvider', message: `Unsupported aiProvider: ${aiProviderRaw}`, suggestion: 'Only openai, codex, or claudeCode are supported.', required: true, }); aiProvider = 'openai'; } config.aiProvider = aiProvider; let codexConfig = config.codex; if (aiProvider === 'codex') { if (!codexConfig) { codexConfig = { apiKey: '', baseURL: 'https://chatgpt.com/backend-api/codex', model: 'gpt-5-codex', timeout: 1800000, reasoning: { effort: 'minimal', summary: 'auto', }, textVerbosity: 'low', authMode: 'ApiKey', }; config.codex = codexConfig; } codexConfig = config.codex; const authModeRaw = (codexConfig.authMode || 'ApiKey') .toString() .trim() .toLowerCase(); codexConfig.authMode = authModeRaw === 'chatgpt' ? 'ChatGPT' : 'ApiKey'; const gatewayApiKeyCandidate = typeof config.gateway?.apiKey === 'string' ? config.gateway.apiKey.trim() : undefined; const trimmedCodexKey = codexConfig.apiKey?.trim(); if (codexConfig.authMode === 'ApiKey') { if (!trimmedCodexKey) { if (gatewayApiKeyCandidate) { codexConfig.apiKey = gatewayApiKeyCandidate; } else { errors.push({ field: 'codex.apiKey', message: 'Codex API key is empty', suggestion: 'Set codex.apiKey in the configuration file.', required: true, }); } } else { codexConfig.apiKey = trimmedCodexKey; } } else { if (trimmedCodexKey) { warnings.push('codex.apiKey is ignored when ChatGPT mode is enabled.'); } codexConfig.apiKey = undefined; } if (!codexConfig.baseURL) { warnings.push('codex.baseURL is not set; using the default value.'); codexConfig.baseURL = 'https://chatgpt.com/backend-api/codex'; } if (!codexConfig.model) { warnings.push('codex.model is not set; using the default value.'); codexConfig.model = 'gpt-5-codex'; } if (!codexConfig.timeout) { warnings.push('codex.timeout is not set; using the default value.'); codexConfig.timeout = 1800000; } if (!codexConfig.reasoning) { codexConfig.reasoning = { effort: 'minimal', summary: 'auto', }; } else { const effortRaw = codexConfig.reasoning.effort; if (effortRaw && typeof effortRaw === 'string' && ['minimal', 'low', 'medium', 'high'].includes(effortRaw.toLowerCase())) { codexConfig.reasoning.effort = effortRaw.toLowerCase(); } else { codexConfig.reasoning.effort = 'minimal'; warnings.push('codex.reasoning.effort is invalid; defaulting to minimal.'); } const summaryRaw = codexConfig.reasoning.summary; if (typeof summaryRaw === 'string') { const normalizedSummary = summaryRaw.toLowerCase(); if (['concise', 'detailed', 'auto'].includes(normalizedSummary)) { codexConfig.reasoning.summary = normalizedSummary; } else { codexConfig.reasoning.summary = 'auto'; warnings.push('codex.reasoning.summary is invalid; defaulting to auto.'); } } else { codexConfig.reasoning.summary = 'auto'; } } if (!codexConfig.textVerbosity) { codexConfig.textVerbosity = 'low'; } } else { const authModeRaw = (codexConfig?.authMode || 'ApiKey') .toString() .trim() .toLowerCase(); const normalizedAuthMode = authModeRaw === 'chatgpt' ? 'ChatGPT' : 'ApiKey'; if (codexConfig) { codexConfig.authMode = normalizedAuthMode; } const shouldKeepCodex = codexConfig && (codexConfig.authMode === 'ChatGPT' || !!codexConfig.apiKey); config.codex = shouldKeepCodex ? codexConfig : undefined; } let claudeConfig = config.claudeCode; const normalizeBetaList = (value) => { if (!value) { return undefined; } if (Array.isArray(value)) { return value .map((item) => typeof item === 'string' ? item.trim() : String(item).trim()) .filter((item) => item.length > 0); } if (typeof value === 'string') { const normalized = value.trim(); if (!normalized) { return undefined; } return normalized .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0); } return undefined; }; if (aiProvider === 'claudeCode') { if (!claudeConfig) { claudeConfig = { apiKey: '', baseURL: 'https://open.bigmodel.cn/api/anthropic', model: 'claude-sonnet-4-5-20250929', timeout: 1800000, anthropicVersion: '2023-06-01', beta: [ 'claude-code-20250219', ], userAgent: 'claude-cli/2.0.1 (external, cli)', xApp: 'cli', dangerousDirectBrowserAccess: true, maxOutputTokens: 64000, extraHeaders: undefined, }; config.claudeCode = claudeConfig; } claudeConfig = config.claudeCode; const trimmedKey = claudeConfig.apiKey?.trim(); if (!trimmedKey) { errors.push({ field: 'claudeCode.apiKey', message: 'Claude Code API key is empty', suggestion: 'Set claudeCode.apiKey in the configuration file.', required: true, }); } else { claudeConfig.apiKey = trimmedKey; } if (!claudeConfig.baseURL) { warnings.push('claudeCode.baseURL is not set; using the default value.'); claudeConfig.baseURL = 'https://open.bigmodel.cn/api/anthropic'; } if (!claudeConfig.model) { warnings.push('claudeCode.model is not set; using the default value.'); claudeConfig.model = 'claude-sonnet-4-5-20250929'; } if (!claudeConfig.timeout) { warnings.push('claudeCode.timeout is not set; using the default value.'); claudeConfig.timeout = 1800000; } if (!claudeConfig.anthropicVersion) { claudeConfig.anthropicVersion = '2023-06-01'; } const betaList = normalizeBetaList(claudeConfig.beta); claudeConfig.beta = betaList && betaList.length > 0 ? betaList : [ 'claude-code-20250219', ]; if (!claudeConfig.userAgent) { claudeConfig.userAgent = 'claude-cli/2.0.1 (external, cli)'; } if (!claudeConfig.xApp) { claudeConfig.xApp = 'cli'; } if (claudeConfig.dangerousDirectBrowserAccess === undefined) { claudeConfig.dangerousDirectBrowserAccess = true; } if (!claudeConfig.maxOutputTokens) { claudeConfig.maxOutputTokens = 64000; } } else { if (claudeConfig && !claudeConfig.apiKey?.trim()) { config.claudeCode = undefined; } else if (claudeConfig) { claudeConfig.apiKey = claudeConfig.apiKey.trim(); claudeConfig.beta = normalizeBetaList(claudeConfig.beta); } } let gatewayConfig = config.gateway; if (!gatewayConfig) { warnings.push('gateway configuration is missing; using default values.'); gatewayConfig = { port: 23062, host: '0.0.0.0', logLevel: 'info', logDir: DEFAULT_GATEWAY_LOG_DIR, requestTimeout: 3600000, apiMode: 'gemini', cliMode: 'gemini', apiKey: undefined, }; config.gateway = gatewayConfig; } if (!gatewayConfig.logDir) { warnings.push('gateway.logDir is not set; using the default value.'); gatewayConfig.logDir = DEFAULT_GATEWAY_LOG_DIR; } gatewayConfig.logDir = this.normalizeLogDir(gatewayConfig.logDir); const parsedTimeout = Number(gatewayConfig.requestTimeout); if (!Number.isFinite(parsedTimeout) || parsedTimeout <= 0) { warnings.push('gateway.requestTimeout is invalid; defaulting to 3600000 milliseconds.'); gatewayConfig.requestTimeout = 3600000; } else { gatewayConfig.requestTimeout = parsedTimeout; } const apiModeRaw = (gatewayConfig.apiMode || 'gemini') .toString() .trim() .toLowerCase(); gatewayConfig.apiMode = apiModeRaw === 'openai' ? 'openai' : 'gemini'; const cliModeRaw = (gatewayConfig.cliMode || 'gemini') .toString() .trim() .toLowerCase(); if (cliModeRaw === 'opencode') { gatewayConfig.cliMode = 'opencode'; gatewayConfig.apiMode = 'openai'; } else if (cliModeRaw === 'crush') { gatewayConfig.cliMode = 'crush'; gatewayConfig.apiMode = 'openai'; } else if (cliModeRaw === 'qwencode') { gatewayConfig.cliMode = 'qwencode'; gatewayConfig.apiMode = 'openai'; } else { gatewayConfig.cliMode = 'gemini'; } if (typeof gatewayConfig.apiKey === 'string') { const trimmed = gatewayConfig.apiKey.trim(); gatewayConfig.apiKey = trimmed.length > 0 ? trimmed : undefined; } else { gatewayConfig.apiKey = undefined; } const isValid = errors.length === 0; const result = { isValid, errors, warnings, }; if (isValid) { result.config = { openai: config.openai ?? openaiConfig, codex: config.codex, claudeCode: config.claudeCode, gateway: config.gateway, aiProvider: config.aiProvider ?? 'openai', configSource: '', isValid: true, }; } return result; } normalizeLogDir(logDir) { if (!logDir || typeof logDir !== 'string') { return DEFAULT_GATEWAY_LOG_DIR; } const trimmed = logDir.trim(); if (!trimmed) { return DEFAULT_GATEWAY_LOG_DIR; } if (trimmed === '~') { return os.homedir(); } if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) { const relative = trimmed.slice(2); return path.join(os.homedir(), relative); } if (trimmed.startsWith('~')) { const relative = trimmed.slice(1).replace(/^[\\/]/, ''); return path.join(os.homedir(), relative); } return path.isAbsolute(trimmed) ? trimmed : path.resolve(trimmed); } }; exports.GlobalConfigService = GlobalConfigService; exports.GlobalConfigService = GlobalConfigService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", []) ], GlobalConfigService); //# sourceMappingURL=global-config.service.js.map