@halilertekin/claude-code-router-config 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +27 -0
- package/LICENSE +30 -0
- package/README.md +320 -0
- package/cli/analytics.js +509 -0
- package/cli/benchmark.js +342 -0
- package/cli/commands.js +300 -0
- package/config/config.json +67 -0
- package/config/intent-router.js +108 -0
- package/config/smart-intent-router.js +543 -0
- package/docs/AGENTSKILLS_INTEGRATION.md +500 -0
- package/docs/AGENTSKILLS_SETUP.md +743 -0
- package/docs/AGENTSKILLS_SETUP_TR.md +736 -0
- package/docs/FULL_DOCUMENTATION.md +510 -0
- package/docs/FULL_DOCUMENTATION_EN.md +526 -0
- package/docs/HOMEBREW_SETUP.md +252 -0
- package/docs/README_EN.md +146 -0
- package/docs/SETUP_PROMPT.md +299 -0
- package/docs/SETUP_PROMPT_EN.md +317 -0
- package/docs/v1.1.0-FEATURES.md +752 -0
- package/install.js +160 -0
- package/install.sh +73 -0
- package/logging/enhanced-logger.js +410 -0
- package/logging/health-monitor.js +472 -0
- package/logging/middleware.js +384 -0
- package/package.json +91 -0
- package/plugins/plugin-manager.js +607 -0
- package/templates/README.md +161 -0
- package/templates/balanced.json +111 -0
- package/templates/cost-optimized.json +96 -0
- package/templates/development.json +104 -0
- package/templates/performance-optimized.json +88 -0
- package/templates/quality-focused.json +105 -0
- package/web-dashboard/public/css/dashboard.css +575 -0
- package/web-dashboard/public/index.html +308 -0
- package/web-dashboard/public/js/dashboard.js +512 -0
- package/web-dashboard/server.js +352 -0
package/install.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Router Config - Interactive Installer
|
|
5
|
+
* For use with @musistudio/claude-code-router
|
|
6
|
+
*
|
|
7
|
+
* Original project: https://github.com/musistudio/claude-code-router
|
|
8
|
+
* Configuration by Halil Ertekin
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const inquirer = require('inquirer');
|
|
15
|
+
const dotenv = require('dotenv');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude-code-router');
|
|
19
|
+
const packageDir = __dirname;
|
|
20
|
+
|
|
21
|
+
async function checkRequirements() {
|
|
22
|
+
console.log(chalk.blue('📋 Checking requirements...'));
|
|
23
|
+
|
|
24
|
+
// Check Node version
|
|
25
|
+
const nodeVersion = process.version;
|
|
26
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
27
|
+
|
|
28
|
+
if (majorVersion < 16) {
|
|
29
|
+
console.error(chalk.red(`❌ Node.js ${majorVersion} detected. Node.js 16+ required.`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
console.log(chalk.green(`✅ Node.js ${nodeVersion}`));
|
|
33
|
+
|
|
34
|
+
// Check for pnpm
|
|
35
|
+
try {
|
|
36
|
+
execSync('pnpm --version', { stdio: 'ignore' });
|
|
37
|
+
console.log(chalk.green('✅ pnpm found'));
|
|
38
|
+
return 'pnpm';
|
|
39
|
+
} catch {
|
|
40
|
+
try {
|
|
41
|
+
execSync('npm --version', { stdio: 'ignore' });
|
|
42
|
+
console.log(chalk.yellow('⚠️ pnpm not found, using npm'));
|
|
43
|
+
return 'npm';
|
|
44
|
+
} catch {
|
|
45
|
+
console.error(chalk.red('❌ Neither pnpm nor npm found'));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function installRouter(packageManager) {
|
|
52
|
+
console.log(chalk.blue('📦 Installing claude-code-router...'));
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const command = `${packageManager} add -g @musistudio/claude-code-router`;
|
|
56
|
+
console.log(chalk.gray(`Running: ${command}`));
|
|
57
|
+
execSync(command, { stdio: 'inherit' });
|
|
58
|
+
console.log(chalk.green('✅ claude-code-router installed'));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(chalk.red('❌ Failed to install claude-code-router'));
|
|
61
|
+
console.error(error.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function setupConfig() {
|
|
67
|
+
console.log(chalk.blue('⚙️ Setting up configuration...'));
|
|
68
|
+
|
|
69
|
+
// Ensure config directory exists
|
|
70
|
+
await fs.ensureDir(configDir);
|
|
71
|
+
|
|
72
|
+
// Copy config files
|
|
73
|
+
const configFiles = ['config.json', 'intent-router.js'];
|
|
74
|
+
for (const file of configFiles) {
|
|
75
|
+
const src = path.join(packageDir, 'config', file);
|
|
76
|
+
const dest = path.join(configDir, file);
|
|
77
|
+
|
|
78
|
+
if (await fs.pathExists(dest)) {
|
|
79
|
+
const { overwrite } = await inquirer.prompt([
|
|
80
|
+
{
|
|
81
|
+
type: 'confirm',
|
|
82
|
+
name: 'overwrite',
|
|
83
|
+
message: `File ${file} exists. Overwrite?`,
|
|
84
|
+
default: false
|
|
85
|
+
}
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
if (!overwrite) {
|
|
89
|
+
console.log(chalk.yellow(`⚠️ Skipping ${file}`));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await fs.copy(src, dest);
|
|
95
|
+
console.log(chalk.green(`✅ ${file} copied`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Copy .env.example if .env doesn't exist
|
|
99
|
+
const envFile = path.join(process.env.HOME || process.env.USERPROFILE, '.env');
|
|
100
|
+
const envExample = path.join(packageDir, '.env.example');
|
|
101
|
+
|
|
102
|
+
if (!(await fs.pathExists(envFile))) {
|
|
103
|
+
await fs.copy(envExample, envFile);
|
|
104
|
+
console.log(chalk.green('✅ .env file created from example'));
|
|
105
|
+
} else {
|
|
106
|
+
console.log(chalk.yellow('⚠️ .env file already exists'));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function showNextSteps() {
|
|
111
|
+
console.log(chalk.green('\n🎉 Installation complete!'));
|
|
112
|
+
console.log(chalk.blue('\n📝 Next steps:'));
|
|
113
|
+
console.log('\n1. Edit your API keys in ~/.env file:');
|
|
114
|
+
console.log(chalk.gray(' nano ~/.env'));
|
|
115
|
+
|
|
116
|
+
console.log('\n2. Add environment variables to your shell (~/.zshrc or ~/.bashrc):');
|
|
117
|
+
console.log(chalk.cyan(`
|
|
118
|
+
# Claude Code Router
|
|
119
|
+
export $(cat ~/.env | xargs)
|
|
120
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
|
|
121
|
+
export NO_PROXY="127.0.0.1"
|
|
122
|
+
`));
|
|
123
|
+
|
|
124
|
+
console.log('\n3. Reload your shell:');
|
|
125
|
+
console.log(chalk.gray(' source ~/.zshrc'));
|
|
126
|
+
|
|
127
|
+
console.log('\n4. Start the router:');
|
|
128
|
+
console.log(chalk.gray(' ccr code'));
|
|
129
|
+
|
|
130
|
+
console.log(chalk.blue('\n📚 Documentation:'));
|
|
131
|
+
console.log(chalk.gray(' https://github.com/halilertekin/claude-code-router-config'));
|
|
132
|
+
|
|
133
|
+
console.log(chalk.blue('\n🔑 Get API keys:'));
|
|
134
|
+
console.log(chalk.gray(' OpenAI: https://platform.openai.com/api-keys'));
|
|
135
|
+
console.log(chalk.gray(' Anthropic: https://console.anthropic.com/settings/keys'));
|
|
136
|
+
console.log(chalk.gray(' Gemini: https://aistudio.google.com/apikey'));
|
|
137
|
+
console.log(chalk.gray(' Qwen: https://dashscope.console.aliyun.com/apiKey'));
|
|
138
|
+
console.log(chalk.gray(' GLM: https://open.bigmodel.cn/usercenter/apikeys'));
|
|
139
|
+
console.log(chalk.gray(' OpenRouter: https://openrouter.ai/keys'));
|
|
140
|
+
console.log(chalk.gray(' Copilot: https://github.com/settings/tokens'));
|
|
141
|
+
|
|
142
|
+
console.log(chalk.yellow('\n⭐ Attribution:'));
|
|
143
|
+
console.log(chalk.gray(' This config is for @musistudio/claude-code-router'));
|
|
144
|
+
console.log(chalk.gray(' Original: https://github.com/musistudio/claude-code-router'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function main() {
|
|
148
|
+
console.log(chalk.cyan.bold('\n🚀 Claude Code Router Config Installer\n'));
|
|
149
|
+
|
|
150
|
+
const packageManager = await checkRequirements();
|
|
151
|
+
await installRouter(packageManager);
|
|
152
|
+
await setupConfig();
|
|
153
|
+
await showNextSteps();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (require.main === module) {
|
|
157
|
+
main().catch(console.error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { checkRequirements, installRouter, setupConfig };
|
package/install.sh
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Claude Code Router - Install Script
|
|
4
|
+
# Usage: ./install.sh
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "=========================================="
|
|
9
|
+
echo " Claude Code Router - Kurulum"
|
|
10
|
+
echo "=========================================="
|
|
11
|
+
echo ""
|
|
12
|
+
|
|
13
|
+
# Colors
|
|
14
|
+
RED='\033[0;31m'
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
NC='\033[0m' # No Color
|
|
18
|
+
|
|
19
|
+
# Check for pnpm
|
|
20
|
+
if ! command -v pnpm &> /dev/null; then
|
|
21
|
+
echo -e "${YELLOW}pnpm bulunamadı. Kuruluyor...${NC}"
|
|
22
|
+
npm install -g pnpm
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Install claude-code-router
|
|
26
|
+
echo -e "${GREEN}[1/3] Claude Code Router kuruluyor...${NC}"
|
|
27
|
+
pnpm add -g @musistudio/claude-code-router
|
|
28
|
+
|
|
29
|
+
# Create config directory
|
|
30
|
+
echo -e "${GREEN}[2/3] Config dizini oluşturuluyor...${NC}"
|
|
31
|
+
mkdir -p ~/.claude-code-router
|
|
32
|
+
|
|
33
|
+
# Copy config files
|
|
34
|
+
echo -e "${GREEN}[3/3] Config dosyaları kopyalanıyor...${NC}"
|
|
35
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
36
|
+
cp "$SCRIPT_DIR/config/config.json" ~/.claude-code-router/
|
|
37
|
+
cp "$SCRIPT_DIR/config/intent-router.js" ~/.claude-code-router/
|
|
38
|
+
|
|
39
|
+
echo ""
|
|
40
|
+
echo -e "${GREEN}=========================================="
|
|
41
|
+
echo " Kurulum Tamamlandı!"
|
|
42
|
+
echo "==========================================${NC}"
|
|
43
|
+
echo ""
|
|
44
|
+
echo -e "${YELLOW}SON ADIM: ~/.zshrc dosyanıza aşağıdaki satırları ekleyin:${NC}"
|
|
45
|
+
echo ""
|
|
46
|
+
cat << 'EOF'
|
|
47
|
+
# ═══════════════════════════════════════════════════
|
|
48
|
+
# Claude Code Router - API Keys
|
|
49
|
+
# ═══════════════════════════════════════════════════
|
|
50
|
+
export OPENAI_API_KEY="sk-..."
|
|
51
|
+
export ANTHROPIC_API_KEY="sk-ant-..."
|
|
52
|
+
export GEMINI_API_KEY="AIza..."
|
|
53
|
+
export QWEN_API_KEY="sk-..."
|
|
54
|
+
export GLM_API_KEY="..."
|
|
55
|
+
export OPENROUTER_API_KEY="sk-or-..."
|
|
56
|
+
|
|
57
|
+
# Router Connection
|
|
58
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
|
|
59
|
+
export NO_PROXY="127.0.0.1"
|
|
60
|
+
EOF
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo -e "${GREEN}Sonra:${NC}"
|
|
64
|
+
echo " source ~/.zshrc"
|
|
65
|
+
echo " ccr code"
|
|
66
|
+
echo ""
|
|
67
|
+
echo -e "${YELLOW}API Key Alma Linkleri:${NC}"
|
|
68
|
+
echo " OpenAI: https://platform.openai.com/api-keys"
|
|
69
|
+
echo " Anthropic: https://console.anthropic.com/settings/keys"
|
|
70
|
+
echo " Gemini: https://aistudio.google.com/apikey"
|
|
71
|
+
echo " Qwen: https://dashscope.console.aliyun.com/apiKey"
|
|
72
|
+
echo " GLM: https://open.bigmodel.cn/usercenter/apikeys"
|
|
73
|
+
echo " OpenRouter: https://openrouter.ai/keys"
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
class EnhancedLogger {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.logDir = options.logDir || path.join(os.homedir(), '.claude-code-router', 'logs');
|
|
10
|
+
this.level = options.level || 'info';
|
|
11
|
+
this.enableConsole = options.enableConsole !== false;
|
|
12
|
+
this.enableFile = options.enableFile !== false;
|
|
13
|
+
this.enableMetrics = options.enableMetrics !== false;
|
|
14
|
+
this.enableAnalytics = options.enableAnalytics !== false;
|
|
15
|
+
this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
|
|
16
|
+
this.maxFiles = options.maxFiles || 5;
|
|
17
|
+
|
|
18
|
+
// Initialize log directory
|
|
19
|
+
this.initLogDirectory();
|
|
20
|
+
|
|
21
|
+
// Current date for log rotation
|
|
22
|
+
this.currentDate = new Date().toISOString().split('T')[0];
|
|
23
|
+
this.logFile = path.join(this.logDir, `claude-router-${this.currentDate}.log`);
|
|
24
|
+
this.metricsFile = path.join(this.logDir, 'metrics.json');
|
|
25
|
+
this.errorFile = path.join(this.logDir, 'errors.log');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
initLogDirectory() {
|
|
29
|
+
if (!fs.existsSync(this.logDir)) {
|
|
30
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Log levels with numeric values for filtering
|
|
35
|
+
static levels = {
|
|
36
|
+
fatal: 0,
|
|
37
|
+
error: 1,
|
|
38
|
+
warn: 2,
|
|
39
|
+
info: 3,
|
|
40
|
+
debug: 4,
|
|
41
|
+
trace: 5
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Check if we should log at this level
|
|
45
|
+
shouldLog(level) {
|
|
46
|
+
return EnhancedLogger.levels[level] <= EnhancedLogger.levels[this.level];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Format log entry
|
|
50
|
+
formatEntry(level, message, meta = {}) {
|
|
51
|
+
const timestamp = new Date().toISOString();
|
|
52
|
+
const pid = process.pid;
|
|
53
|
+
const entry = {
|
|
54
|
+
timestamp,
|
|
55
|
+
level: level.toUpperCase(),
|
|
56
|
+
pid,
|
|
57
|
+
message,
|
|
58
|
+
...meta
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Format for console
|
|
62
|
+
const consoleMessage = `[${timestamp}] ${level.toUpperCase()} ${message}`;
|
|
63
|
+
|
|
64
|
+
return { entry, consoleMessage };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Write to file with rotation
|
|
68
|
+
writeToFile(entry) {
|
|
69
|
+
if (!this.enableFile) return;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Check if we need to rotate log file
|
|
73
|
+
if (fs.existsSync(this.logFile)) {
|
|
74
|
+
const stats = fs.statSync(this.logFile);
|
|
75
|
+
if (stats.size > this.maxFileSize) {
|
|
76
|
+
this.rotateLog();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const logLine = JSON.stringify(entry) + '\n';
|
|
81
|
+
fs.appendFileSync(this.logFile, logLine);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Failed to write to log file:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Log rotation
|
|
88
|
+
rotateLog() {
|
|
89
|
+
const baseName = path.join(this.logDir, `claude-router-${this.currentDate}`);
|
|
90
|
+
|
|
91
|
+
// Rotate existing files
|
|
92
|
+
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
93
|
+
const oldFile = `${baseName}.${i}.log`;
|
|
94
|
+
const newFile = `${baseName}.${i + 1}.log`;
|
|
95
|
+
|
|
96
|
+
if (fs.existsSync(oldFile)) {
|
|
97
|
+
if (i === this.maxFiles - 1) {
|
|
98
|
+
fs.unlinkSync(oldFile); // Delete oldest
|
|
99
|
+
} else {
|
|
100
|
+
fs.renameSync(oldFile, newFile);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Move current log to .1
|
|
106
|
+
if (fs.existsSync(this.logFile)) {
|
|
107
|
+
fs.renameSync(this.logFile, `${baseName}.1.log`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Log to console with colors
|
|
112
|
+
writeToConsole(level, consoleMessage) {
|
|
113
|
+
if (!this.enableConsole) return;
|
|
114
|
+
|
|
115
|
+
let coloredMessage;
|
|
116
|
+
switch (level) {
|
|
117
|
+
case 'fatal':
|
|
118
|
+
coloredMessage = chalk.red.bold(consoleMessage);
|
|
119
|
+
break;
|
|
120
|
+
case 'error':
|
|
121
|
+
coloredMessage = chalk.red(consoleMessage);
|
|
122
|
+
break;
|
|
123
|
+
case 'warn':
|
|
124
|
+
coloredMessage = chalk.yellow(consoleMessage);
|
|
125
|
+
break;
|
|
126
|
+
case 'info':
|
|
127
|
+
coloredMessage = chalk.blue(consoleMessage);
|
|
128
|
+
break;
|
|
129
|
+
case 'debug':
|
|
130
|
+
coloredMessage = chalk.magenta(consoleMessage);
|
|
131
|
+
break;
|
|
132
|
+
case 'trace':
|
|
133
|
+
coloredMessage = chalk.gray(consoleMessage);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
coloredMessage = consoleMessage;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(coloredMessage);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Log entry method
|
|
143
|
+
log(level, message, meta = {}) {
|
|
144
|
+
if (!this.shouldLog(level)) return;
|
|
145
|
+
|
|
146
|
+
const { entry, consoleMessage } = this.formatEntry(level, message, meta);
|
|
147
|
+
|
|
148
|
+
// Write to console
|
|
149
|
+
this.writeToConsole(level, consoleMessage);
|
|
150
|
+
|
|
151
|
+
// Write to file
|
|
152
|
+
this.writeToFile(entry);
|
|
153
|
+
|
|
154
|
+
// Handle errors specially
|
|
155
|
+
if (level === 'error' || level === 'fatal') {
|
|
156
|
+
this.logError(entry);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update metrics
|
|
160
|
+
if (this.enableMetrics) {
|
|
161
|
+
this.updateMetrics(level, meta);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Send to analytics if enabled
|
|
165
|
+
if (this.enableAnalytics && meta.provider && meta.model) {
|
|
166
|
+
this.sendToAnalytics(entry);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Log errors to separate file
|
|
171
|
+
logError(entry) {
|
|
172
|
+
try {
|
|
173
|
+
const errorLine = JSON.stringify(entry) + '\n';
|
|
174
|
+
fs.appendFileSync(this.errorFile, errorLine);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Failed to write to error log:', error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update metrics
|
|
181
|
+
updateMetrics(level, meta) {
|
|
182
|
+
try {
|
|
183
|
+
let metrics = {};
|
|
184
|
+
|
|
185
|
+
if (fs.existsSync(this.metricsFile)) {
|
|
186
|
+
metrics = JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const now = new Date().toISOString();
|
|
190
|
+
|
|
191
|
+
// Initialize metrics structure
|
|
192
|
+
if (!metrics.requests) metrics.requests = { total: 0, byLevel: {}, byProvider: {} };
|
|
193
|
+
if (!metrics.latency) metrics.latency = { avg: 0, min: Infinity, max: 0, samples: [] };
|
|
194
|
+
if (!metrics.errors) metrics.errors = { total: 0, byProvider: {} };
|
|
195
|
+
if (!metrics.costs) metrics.costs = { total: 0, byProvider: {} };
|
|
196
|
+
if (!metrics.uptime) metrics.uptime = { start: metrics.uptime?.start || now, lastActivity: now };
|
|
197
|
+
|
|
198
|
+
// Update request counts
|
|
199
|
+
metrics.requests.total++;
|
|
200
|
+
metrics.requests.byLevel[level] = (metrics.requests.byLevel[level] || 0) + 1;
|
|
201
|
+
|
|
202
|
+
if (meta.provider) {
|
|
203
|
+
metrics.requests.byProvider[meta.provider] = (metrics.requests.byProvider[meta.provider] || 0) + 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Update latency
|
|
207
|
+
if (meta.latency) {
|
|
208
|
+
metrics.latency.samples.push(meta.latency);
|
|
209
|
+
metrics.latency.min = Math.min(metrics.latency.min, meta.latency);
|
|
210
|
+
metrics.latency.max = Math.max(metrics.latency.max, meta.latency);
|
|
211
|
+
|
|
212
|
+
// Keep only last 1000 samples for performance
|
|
213
|
+
if (metrics.latency.samples.length > 1000) {
|
|
214
|
+
metrics.latency.samples = metrics.latency.samples.slice(-1000);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
metrics.latency.avg = Math.round(
|
|
218
|
+
metrics.latency.samples.reduce((a, b) => a + b, 0) / metrics.latency.samples.length
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Update errors
|
|
223
|
+
if (level === 'error' || level === 'fatal') {
|
|
224
|
+
metrics.errors.total++;
|
|
225
|
+
if (meta.provider) {
|
|
226
|
+
metrics.errors.byProvider[meta.provider] = (metrics.errors.byProvider[meta.provider] || 0) + 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update costs
|
|
231
|
+
if (meta.cost) {
|
|
232
|
+
metrics.costs.total += meta.cost;
|
|
233
|
+
if (meta.provider) {
|
|
234
|
+
metrics.costs.byProvider[meta.provider] = (metrics.costs.byProvider[meta.provider] || 0) + meta.cost;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Update uptime
|
|
239
|
+
metrics.uptime.lastActivity = now;
|
|
240
|
+
|
|
241
|
+
fs.writeFileSync(this.metricsFile, JSON.stringify(metrics, null, 2));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error('Failed to update metrics:', error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Send to analytics module
|
|
248
|
+
sendToAnalytics(entry) {
|
|
249
|
+
if (!entry.meta || !entry.meta.provider || !entry.meta.model) return;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const analyticsPath = path.join(__dirname, '..', 'cli', 'analytics.js');
|
|
253
|
+
|
|
254
|
+
// Call analytics module if available
|
|
255
|
+
const child = spawn('node', [analyticsPath, 'record',
|
|
256
|
+
entry.meta.provider,
|
|
257
|
+
entry.meta.model,
|
|
258
|
+
entry.meta.inputTokens || 0,
|
|
259
|
+
entry.meta.outputTokens || 0,
|
|
260
|
+
entry.meta.latency || 0,
|
|
261
|
+
entry.success !== false
|
|
262
|
+
], {
|
|
263
|
+
stdio: 'ignore',
|
|
264
|
+
detached: true
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
child.unref();
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Silently fail analytics - don't break logging
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Convenience methods
|
|
274
|
+
fatal(message, meta = {}) {
|
|
275
|
+
this.log('fatal', message, { ...meta, stack: new Error().stack });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
error(message, meta = {}) {
|
|
279
|
+
this.log('error', message, meta);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
warn(message, meta = {}) {
|
|
283
|
+
this.log('warn', message, meta);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
info(message, meta = {}) {
|
|
287
|
+
this.log('info', message, meta);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
debug(message, meta = {}) {
|
|
291
|
+
this.log('debug', message, meta);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
trace(message, meta = {}) {
|
|
295
|
+
this.log('trace', message, meta);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Request logging with standard format
|
|
299
|
+
logRequest(provider, model, inputTokens, outputTokens, latency, success, cost = null) {
|
|
300
|
+
this.info('API Request', {
|
|
301
|
+
event: 'api_request',
|
|
302
|
+
provider,
|
|
303
|
+
model,
|
|
304
|
+
inputTokens,
|
|
305
|
+
outputTokens,
|
|
306
|
+
totalTokens: inputTokens + outputTokens,
|
|
307
|
+
latency,
|
|
308
|
+
success,
|
|
309
|
+
cost,
|
|
310
|
+
timestamp: new Date().toISOString()
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Route decision logging
|
|
315
|
+
logRoute(request, selectedProvider, selectedModel, reason, alternatives = []) {
|
|
316
|
+
this.debug('Route Decision', {
|
|
317
|
+
event: 'route_decision',
|
|
318
|
+
requestId: request.id || 'unknown',
|
|
319
|
+
requestType: request.type || 'unknown',
|
|
320
|
+
selectedProvider,
|
|
321
|
+
selectedModel,
|
|
322
|
+
reason,
|
|
323
|
+
alternatives,
|
|
324
|
+
timestamp: new Date().toISOString()
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Health check logging
|
|
329
|
+
logHealthCheck(provider, status, latency = null, error = null) {
|
|
330
|
+
const level = status === 'healthy' ? 'info' : 'warn';
|
|
331
|
+
this.log(level, `Health Check - ${provider}`, {
|
|
332
|
+
event: 'health_check',
|
|
333
|
+
provider,
|
|
334
|
+
status,
|
|
335
|
+
latency,
|
|
336
|
+
error,
|
|
337
|
+
timestamp: new Date().toISOString()
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Get metrics summary
|
|
342
|
+
getMetrics() {
|
|
343
|
+
try {
|
|
344
|
+
if (fs.existsSync(this.metricsFile)) {
|
|
345
|
+
return JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('Failed to read metrics:', error);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get recent logs
|
|
354
|
+
getRecentLogs(count = 100, level = null) {
|
|
355
|
+
try {
|
|
356
|
+
if (!fs.existsSync(this.logFile)) return [];
|
|
357
|
+
|
|
358
|
+
const content = fs.readFileSync(this.logFile, 'utf8');
|
|
359
|
+
const lines = content.trim().split('\n').filter(line => line);
|
|
360
|
+
const logs = lines.map(line => {
|
|
361
|
+
try {
|
|
362
|
+
return JSON.parse(line);
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}).filter(Boolean);
|
|
367
|
+
|
|
368
|
+
// Filter by level if specified
|
|
369
|
+
if (level) {
|
|
370
|
+
return logs.filter(log => log.level === level.toUpperCase()).slice(-count);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return logs.slice(-count);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('Failed to read recent logs:', error);
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Clean old logs
|
|
381
|
+
cleanup() {
|
|
382
|
+
try {
|
|
383
|
+
const files = fs.readdirSync(this.logDir);
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
386
|
+
|
|
387
|
+
files.forEach(file => {
|
|
388
|
+
if (file.endsWith('.log') || file.endsWith('.json')) {
|
|
389
|
+
const filePath = path.join(this.logDir, file);
|
|
390
|
+
const stats = fs.statSync(filePath);
|
|
391
|
+
|
|
392
|
+
if (now - stats.mtime.getTime() > maxAge) {
|
|
393
|
+
fs.unlinkSync(filePath);
|
|
394
|
+
console.log(`Cleaned up old log file: ${file}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Failed to cleanup logs:', error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Export singleton instance
|
|
405
|
+
const logger = new EnhancedLogger();
|
|
406
|
+
|
|
407
|
+
module.exports = {
|
|
408
|
+
EnhancedLogger,
|
|
409
|
+
logger
|
|
410
|
+
};
|