@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
JavaScript
"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