vettcode-cli 1.0.0
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 +20 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/ast-extractor.js +519 -0
- package/dist/cli-scan-orchestrator.js +336 -0
- package/dist/cli.js +208 -0
- package/dist/control-flow-analyzer.js +184 -0
- package/dist/data-flow-analyzer.js +197 -0
- package/dist/enhanced-patterns.js +457 -0
- package/dist/file-collector.js +132 -0
- package/dist/ignore-patterns.js +225 -0
- package/dist/openrouter.js +311 -0
- package/dist/patterns.js +248 -0
- package/dist/prompts.js +144 -0
- package/dist/reference-graph.js +415 -0
- package/dist/report-generator.js +128 -0
- package/dist/scan-priority.js +49 -0
- package/dist/smart-scan-orchestrator.js +878 -0
- package/dist/static-analyzer.js +1681 -0
- package/dist/types.js +2 -0
- package/dist/verification-layer.js +525 -0
- package/package.json +61 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEXT_EXTENSIONS = exports.IGNORED_EXTENSIONS = exports.IGNORED_FILE_NAMES = exports.IGNORED_DIR_NAMES = void 0;
|
|
4
|
+
exports.shouldIgnorePath = shouldIgnorePath;
|
|
5
|
+
/** Directory and file patterns excluded from vetting (deps, build output, VCS, etc.) */
|
|
6
|
+
exports.IGNORED_DIR_NAMES = new Set([
|
|
7
|
+
"node_modules",
|
|
8
|
+
".git",
|
|
9
|
+
".github",
|
|
10
|
+
".svn",
|
|
11
|
+
".hg",
|
|
12
|
+
"dist",
|
|
13
|
+
"build",
|
|
14
|
+
"out",
|
|
15
|
+
".next",
|
|
16
|
+
".nuxt",
|
|
17
|
+
".output",
|
|
18
|
+
"coverage",
|
|
19
|
+
".cache",
|
|
20
|
+
".turbo",
|
|
21
|
+
"vendor",
|
|
22
|
+
"bower_components",
|
|
23
|
+
"__pycache__",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".mypy_cache",
|
|
26
|
+
".venv",
|
|
27
|
+
"venv",
|
|
28
|
+
"env",
|
|
29
|
+
".env",
|
|
30
|
+
"target",
|
|
31
|
+
"obj",
|
|
32
|
+
".gradle",
|
|
33
|
+
".idea",
|
|
34
|
+
".vscode",
|
|
35
|
+
"Pods",
|
|
36
|
+
"DerivedData",
|
|
37
|
+
".parcel-cache",
|
|
38
|
+
".svelte-kit",
|
|
39
|
+
"storybook-static",
|
|
40
|
+
".vercel",
|
|
41
|
+
"pkg",
|
|
42
|
+
"deps",
|
|
43
|
+
"_build",
|
|
44
|
+
"elm-stuff",
|
|
45
|
+
".stack-work",
|
|
46
|
+
"zig-cache",
|
|
47
|
+
"zig-out",
|
|
48
|
+
"test", // Test directories
|
|
49
|
+
"tests", // Test directories
|
|
50
|
+
"__tests__", // Jest test directories
|
|
51
|
+
"spec", // Spec directories
|
|
52
|
+
"specs", // Spec directories
|
|
53
|
+
"e2e", // End-to-end test directories
|
|
54
|
+
"cypress", // Cypress test directories
|
|
55
|
+
"playwright", // Playwright test directories
|
|
56
|
+
".jest", // Jest config directories
|
|
57
|
+
".mocha", // Mocha config directories
|
|
58
|
+
]);
|
|
59
|
+
exports.IGNORED_FILE_NAMES = new Set([
|
|
60
|
+
".DS_Store",
|
|
61
|
+
"Thumbs.db",
|
|
62
|
+
"package-lock.json",
|
|
63
|
+
"yarn.lock",
|
|
64
|
+
"pnpm-lock.yaml",
|
|
65
|
+
"Cargo.lock",
|
|
66
|
+
"composer.lock",
|
|
67
|
+
"Gemfile.lock",
|
|
68
|
+
"poetry.lock",
|
|
69
|
+
"README.md",
|
|
70
|
+
"CHANGELOG.md",
|
|
71
|
+
"CONTRIBUTING.md",
|
|
72
|
+
"LICENSE.md",
|
|
73
|
+
"CODE_OF_CONDUCT.md",
|
|
74
|
+
]);
|
|
75
|
+
exports.IGNORED_EXTENSIONS = new Set([
|
|
76
|
+
".png",
|
|
77
|
+
".jpg",
|
|
78
|
+
".jpeg",
|
|
79
|
+
".gif",
|
|
80
|
+
".webp",
|
|
81
|
+
".ico",
|
|
82
|
+
".svg",
|
|
83
|
+
".woff",
|
|
84
|
+
".woff2",
|
|
85
|
+
".ttf",
|
|
86
|
+
".eot",
|
|
87
|
+
".mp4",
|
|
88
|
+
".mp3",
|
|
89
|
+
".wav",
|
|
90
|
+
".zip",
|
|
91
|
+
".tar",
|
|
92
|
+
".gz",
|
|
93
|
+
".rar",
|
|
94
|
+
".7z",
|
|
95
|
+
".pdf",
|
|
96
|
+
".exe",
|
|
97
|
+
".dll",
|
|
98
|
+
".so",
|
|
99
|
+
".dylib",
|
|
100
|
+
".wasm",
|
|
101
|
+
".map",
|
|
102
|
+
".min.js",
|
|
103
|
+
".min.css",
|
|
104
|
+
".lock",
|
|
105
|
+
".pyc",
|
|
106
|
+
".class",
|
|
107
|
+
".jar",
|
|
108
|
+
".o",
|
|
109
|
+
".a",
|
|
110
|
+
".md", // Markdown files - documentation only
|
|
111
|
+
".mdx", // MDX files - documentation only
|
|
112
|
+
]);
|
|
113
|
+
exports.TEXT_EXTENSIONS = new Set([
|
|
114
|
+
".ts",
|
|
115
|
+
".tsx",
|
|
116
|
+
".js",
|
|
117
|
+
".jsx",
|
|
118
|
+
".mjs",
|
|
119
|
+
".cjs",
|
|
120
|
+
".json",
|
|
121
|
+
".jsonc",
|
|
122
|
+
".vue",
|
|
123
|
+
".svelte",
|
|
124
|
+
".py",
|
|
125
|
+
".rb",
|
|
126
|
+
".go",
|
|
127
|
+
".rs",
|
|
128
|
+
".java",
|
|
129
|
+
".kt",
|
|
130
|
+
".kts",
|
|
131
|
+
".scala",
|
|
132
|
+
".php",
|
|
133
|
+
".cs",
|
|
134
|
+
".cpp",
|
|
135
|
+
".c",
|
|
136
|
+
".h",
|
|
137
|
+
".hpp",
|
|
138
|
+
".swift",
|
|
139
|
+
".dart",
|
|
140
|
+
".lua",
|
|
141
|
+
".sh",
|
|
142
|
+
".bash",
|
|
143
|
+
".zsh",
|
|
144
|
+
".ps1",
|
|
145
|
+
".sql",
|
|
146
|
+
".graphql",
|
|
147
|
+
".gql",
|
|
148
|
+
".yaml",
|
|
149
|
+
".yml",
|
|
150
|
+
".toml",
|
|
151
|
+
".xml",
|
|
152
|
+
".html",
|
|
153
|
+
".htm",
|
|
154
|
+
".css",
|
|
155
|
+
".scss",
|
|
156
|
+
".sass",
|
|
157
|
+
".less",
|
|
158
|
+
".env.example",
|
|
159
|
+
".prisma",
|
|
160
|
+
".proto",
|
|
161
|
+
".tf",
|
|
162
|
+
".hcl",
|
|
163
|
+
".dockerfile",
|
|
164
|
+
".gradle",
|
|
165
|
+
".properties",
|
|
166
|
+
".ini",
|
|
167
|
+
".cfg",
|
|
168
|
+
".conf",
|
|
169
|
+
".env.local.example",
|
|
170
|
+
]);
|
|
171
|
+
function shouldIgnorePath(relativePath) {
|
|
172
|
+
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
173
|
+
const parts = normalized.split("/");
|
|
174
|
+
for (const part of parts) {
|
|
175
|
+
if (exports.IGNORED_DIR_NAMES.has(part))
|
|
176
|
+
return true;
|
|
177
|
+
if (part.startsWith(".") && part !== ".env.example" && part !== ".gitignore") {
|
|
178
|
+
if ([".github", ".gitlab", ".circleci"].includes(part))
|
|
179
|
+
continue;
|
|
180
|
+
if (part.length > 1 && !part.includes("env"))
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const fileName = parts[parts.length - 1] ?? "";
|
|
185
|
+
if (exports.IGNORED_FILE_NAMES.has(fileName))
|
|
186
|
+
return true;
|
|
187
|
+
// Ignore test files by pattern
|
|
188
|
+
const testPatterns = [
|
|
189
|
+
/\.test\.(ts|tsx|js|jsx)$/, // file.test.ts
|
|
190
|
+
/\.spec\.(ts|tsx|js|jsx)$/, // file.spec.ts
|
|
191
|
+
/\.test\.(py|rb|go|java|php)$/, // file.test.py
|
|
192
|
+
/\.spec\.(py|rb|go|java|php)$/, // file.spec.py
|
|
193
|
+
/_test\.(ts|tsx|js|jsx|py|go)$/, // file_test.go
|
|
194
|
+
/_spec\.(ts|tsx|js|jsx|py|rb)$/, // file_spec.rb
|
|
195
|
+
/test_.*\.(py|rb)$/, // test_file.py
|
|
196
|
+
/.*\.e2e\.(ts|tsx|js|jsx)$/, // file.e2e.ts
|
|
197
|
+
/.*\.integration\.(ts|tsx|js|jsx)$/, // file.integration.ts
|
|
198
|
+
];
|
|
199
|
+
for (const pattern of testPatterns) {
|
|
200
|
+
if (pattern.test(fileName))
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
const dot = fileName.lastIndexOf(".");
|
|
204
|
+
const ext = dot >= 0 ? fileName.slice(dot).toLowerCase() : "";
|
|
205
|
+
if (ext && exports.IGNORED_EXTENSIONS.has(ext))
|
|
206
|
+
return true;
|
|
207
|
+
if (!ext && !fileName.includes(".")) {
|
|
208
|
+
const noExtAllowed = new Set([
|
|
209
|
+
"dockerfile",
|
|
210
|
+
"makefile",
|
|
211
|
+
"gemfile",
|
|
212
|
+
"rakefile",
|
|
213
|
+
"procfile",
|
|
214
|
+
"vagrantfile",
|
|
215
|
+
]);
|
|
216
|
+
if (!noExtAllowed.has(fileName.toLowerCase())) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (ext && !exports.TEXT_EXTENSIONS.has(ext)) {
|
|
221
|
+
if (!fileName.endsWith(".d.ts"))
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getApiKeys = getApiKeys;
|
|
4
|
+
exports.getModels = getModels;
|
|
5
|
+
exports.nextApiKey = nextApiKey;
|
|
6
|
+
exports.keyForIndex = keyForIndex;
|
|
7
|
+
exports.chatCompletion = chatCompletion;
|
|
8
|
+
exports.parseJsonFromModel = parseJsonFromModel;
|
|
9
|
+
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
10
|
+
let keyIndex = 0;
|
|
11
|
+
function getApiKeys() {
|
|
12
|
+
const keys = [];
|
|
13
|
+
const combined = process.env.OPENROUTER_API_KEYS;
|
|
14
|
+
if (combined) {
|
|
15
|
+
keys.push(...combined
|
|
16
|
+
.split(",")
|
|
17
|
+
.map((k) => k.trim())
|
|
18
|
+
.filter(Boolean));
|
|
19
|
+
}
|
|
20
|
+
for (let i = 1; i <= 3; i++) {
|
|
21
|
+
const k = process.env[`OPENROUTER_API_KEY_${i}`];
|
|
22
|
+
if (k?.trim())
|
|
23
|
+
keys.push(k.trim());
|
|
24
|
+
}
|
|
25
|
+
// Security: Never log API keys or their partial values
|
|
26
|
+
if (keys.length === 0) {
|
|
27
|
+
console.error('[OpenRouter] No API keys found! Check environment variables.');
|
|
28
|
+
// Don't log which specific env vars are set/unset as this could leak information
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const nodeEnv = process.env.NODE_ENV?.trim() || 'production';
|
|
32
|
+
if (nodeEnv === 'development') {
|
|
33
|
+
console.log(`[OpenRouter] Found ${keys.length} API key(s) configured`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return [...new Set(keys)];
|
|
37
|
+
}
|
|
38
|
+
function getModels() {
|
|
39
|
+
const raw = process.env.OPENROUTER_MODELS ??
|
|
40
|
+
"openrouter/free,deepseek/deepseek-chat-v3-0324:free,qwen/qwen-2.5-coder-32b-instruct:free";
|
|
41
|
+
const models = raw
|
|
42
|
+
.split(",")
|
|
43
|
+
.map((m) => m.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
// OpenRouter allows max 3 models in fallback array
|
|
46
|
+
return models.slice(0, 3);
|
|
47
|
+
}
|
|
48
|
+
// Rate limiting per API key to prevent exhaustion
|
|
49
|
+
const keyUsageMap = new Map();
|
|
50
|
+
const RATE_LIMIT_WINDOW = 60000; // 1 minute window
|
|
51
|
+
const MAX_REQUESTS_PER_KEY = 50; // Reduced from 100 to be more conservative
|
|
52
|
+
const LOCK_DURATION = 120000; // 2 minutes lock after exceeding limit
|
|
53
|
+
function nextApiKey() {
|
|
54
|
+
const keys = getApiKeys();
|
|
55
|
+
if (keys.length === 0) {
|
|
56
|
+
throw new Error("No OpenRouter API keys configured. Set OPENROUTER_API_KEY_1, _2, _3 or OPENROUTER_API_KEYS.");
|
|
57
|
+
}
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
// Try to find a key that hasn't exceeded rate limit and isn't locked
|
|
60
|
+
for (let i = 0; i < keys.length; i++) {
|
|
61
|
+
const key = keys[(keyIndex + i) % keys.length];
|
|
62
|
+
const usage = keyUsageMap.get(key);
|
|
63
|
+
// Check if key is locked
|
|
64
|
+
if (usage?.lockUntil && now < usage.lockUntil) {
|
|
65
|
+
continue; // Skip locked keys
|
|
66
|
+
}
|
|
67
|
+
// Reset usage if window has expired
|
|
68
|
+
if (usage && now - usage.resetTime > RATE_LIMIT_WINDOW) {
|
|
69
|
+
keyUsageMap.set(key, { count: 1, resetTime: now });
|
|
70
|
+
keyIndex = (keyIndex + i + 1) % keys.length;
|
|
71
|
+
return key;
|
|
72
|
+
}
|
|
73
|
+
// Check if key is under rate limit
|
|
74
|
+
if (!usage) {
|
|
75
|
+
keyUsageMap.set(key, { count: 1, resetTime: now });
|
|
76
|
+
keyIndex = (keyIndex + i + 1) % keys.length;
|
|
77
|
+
return key;
|
|
78
|
+
}
|
|
79
|
+
if (usage.count < MAX_REQUESTS_PER_KEY) {
|
|
80
|
+
keyUsageMap.set(key, { ...usage, count: usage.count + 1 });
|
|
81
|
+
keyIndex = (keyIndex + i + 1) % keys.length;
|
|
82
|
+
return key;
|
|
83
|
+
}
|
|
84
|
+
// Lock the key if it exceeded the limit
|
|
85
|
+
if (!usage.lockUntil) {
|
|
86
|
+
console.warn(`[OpenRouter] API key ${i + 1} exceeded rate limit, locking for 2 minutes`);
|
|
87
|
+
keyUsageMap.set(key, { ...usage, lockUntil: now + LOCK_DURATION });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// All keys are rate limited or locked
|
|
91
|
+
const unlockTime = Math.min(...Array.from(keyUsageMap.values()).map(u => u.lockUntil || u.resetTime + RATE_LIMIT_WINDOW));
|
|
92
|
+
const waitTime = Math.ceil((unlockTime - now) / 1000);
|
|
93
|
+
throw new Error(`All API keys are rate limited. Please wait ${waitTime} seconds before retrying.`);
|
|
94
|
+
}
|
|
95
|
+
function keyForIndex(index) {
|
|
96
|
+
const keys = getApiKeys();
|
|
97
|
+
if (keys.length === 0)
|
|
98
|
+
throw new Error("No OpenRouter API keys configured.");
|
|
99
|
+
return keys[index % keys.length];
|
|
100
|
+
}
|
|
101
|
+
async function chatCompletion(messages, keyOverride, retries = 2) {
|
|
102
|
+
// Input validation to prevent prompt injection
|
|
103
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
104
|
+
throw new Error('Messages must be a non-empty array');
|
|
105
|
+
}
|
|
106
|
+
// Validate each message structure
|
|
107
|
+
for (const msg of messages) {
|
|
108
|
+
if (!msg || typeof msg !== 'object') {
|
|
109
|
+
throw new Error('Invalid message structure');
|
|
110
|
+
}
|
|
111
|
+
if (!['system', 'user', 'assistant'].includes(msg.role)) {
|
|
112
|
+
throw new Error(`Invalid message role: ${msg.role}`);
|
|
113
|
+
}
|
|
114
|
+
if (typeof msg.content !== 'string') {
|
|
115
|
+
throw new Error('Message content must be a string');
|
|
116
|
+
}
|
|
117
|
+
// Limit message length to prevent abuse
|
|
118
|
+
if (msg.content.length > 1000000) { // 1MB limit
|
|
119
|
+
throw new Error('Message content exceeds maximum length');
|
|
120
|
+
}
|
|
121
|
+
// Check for suspicious patterns that might indicate prompt injection
|
|
122
|
+
const suspiciousPatterns = [
|
|
123
|
+
/ignore\s+(?:all\s+)?previous\s+(?:instructions?|prompts?)/gi,
|
|
124
|
+
/disregard\s+(?:all\s+)?(?:previous|prior)\s+(?:instructions?|commands?)/gi,
|
|
125
|
+
/forget\s+(?:everything|all)\s+(?:before|above)/gi,
|
|
126
|
+
/new\s+instructions?:/gi,
|
|
127
|
+
];
|
|
128
|
+
for (const pattern of suspiciousPatterns) {
|
|
129
|
+
if (pattern.test(msg.content)) {
|
|
130
|
+
console.warn('[Security] Suspicious prompt injection pattern detected');
|
|
131
|
+
// Don't block, but log for monitoring
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const apiKey = keyOverride ?? nextApiKey();
|
|
136
|
+
const models = getModels();
|
|
137
|
+
// Validate and construct site URL
|
|
138
|
+
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
139
|
+
const publicUrl = process.env.NEXT_PUBLIC_SITE_URL?.trim();
|
|
140
|
+
const siteUrl = publicUrl || (vercelUrl ? `https://${vercelUrl}` : "http://localhost:3000");
|
|
141
|
+
const body = {
|
|
142
|
+
models,
|
|
143
|
+
messages,
|
|
144
|
+
temperature: 0.15, // Slightly higher for more nuanced analysis
|
|
145
|
+
max_tokens: 6000, // Increased for detailed findings
|
|
146
|
+
};
|
|
147
|
+
if (models.length === 1) {
|
|
148
|
+
body.model = models[0];
|
|
149
|
+
delete body.models;
|
|
150
|
+
}
|
|
151
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
152
|
+
try {
|
|
153
|
+
const nodeEnv = process.env.NODE_ENV?.trim() || 'production';
|
|
154
|
+
if (nodeEnv === 'development') {
|
|
155
|
+
console.log(`[OpenRouter] Attempt ${attempt + 1}/${retries + 1} - Calling ${OPENROUTER_URL}`);
|
|
156
|
+
console.log(`[OpenRouter] Models: ${JSON.stringify(models)}`);
|
|
157
|
+
console.log(`[OpenRouter] Message count: ${messages.length}, Total chars: ${messages.reduce((sum, m) => sum + m.content.length, 0)}`);
|
|
158
|
+
}
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
|
|
161
|
+
const res = await fetch(OPENROUTER_URL, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
Authorization: `Bearer ${apiKey}`,
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
"HTTP-Referer": siteUrl,
|
|
167
|
+
"X-Title": "Vettcode Engine",
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
});
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
if (nodeEnv === 'development') {
|
|
174
|
+
console.log(`[OpenRouter] Response status: ${res.status}`);
|
|
175
|
+
}
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const errText = await res.text();
|
|
178
|
+
console.error(`[OpenRouter] Error response:`, errText.slice(0, 500));
|
|
179
|
+
// Check for rate limit or temporary errors
|
|
180
|
+
if (res.status === 429 || res.status === 503) {
|
|
181
|
+
if (attempt < retries) {
|
|
182
|
+
console.warn(`[OpenRouter] Rate limited or service unavailable, retrying in ${2000 * (attempt + 1)}ms...`);
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 2000 * (attempt + 1)));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`OpenRouter ${res.status}: ${errText.slice(0, 500)}`);
|
|
188
|
+
}
|
|
189
|
+
const data = (await res.json());
|
|
190
|
+
const content = data.choices?.[0]?.message?.content?.trim();
|
|
191
|
+
if (nodeEnv === 'development') {
|
|
192
|
+
console.log(`[OpenRouter] Response model: ${data.model || 'unknown'}`);
|
|
193
|
+
console.log(`[OpenRouter] Content length: ${content?.length || 0} chars`);
|
|
194
|
+
}
|
|
195
|
+
if (!content) {
|
|
196
|
+
if (attempt < retries) {
|
|
197
|
+
console.warn(`[OpenRouter] Empty response, retrying (${attempt + 1}/${retries})...`);
|
|
198
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
throw new Error("Empty response from OpenRouter after retries");
|
|
202
|
+
}
|
|
203
|
+
if (nodeEnv === 'development') {
|
|
204
|
+
console.log(`[OpenRouter] ✓ Success on attempt ${attempt + 1}`);
|
|
205
|
+
}
|
|
206
|
+
return { content, model: data.model ?? models[0] };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// Log error for debugging
|
|
210
|
+
if (error instanceof Error) {
|
|
211
|
+
console.error(`[OpenRouter] Request error: ${error.message}`);
|
|
212
|
+
}
|
|
213
|
+
if (attempt === retries) {
|
|
214
|
+
console.error(`[OpenRouter] ✗ All attempts failed:`, error);
|
|
215
|
+
// Return a graceful fallback instead of throwing
|
|
216
|
+
throw new Error(`API request failed after ${retries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
217
|
+
}
|
|
218
|
+
console.warn(`[OpenRouter] Attempt ${attempt + 1} failed, retrying...`, error);
|
|
219
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw new Error("Failed after all retries");
|
|
223
|
+
}
|
|
224
|
+
function parseJsonFromModel(raw) {
|
|
225
|
+
const trimmed = raw.trim();
|
|
226
|
+
// Validate input is a string and not empty
|
|
227
|
+
if (typeof trimmed !== 'string' || trimmed.length === 0) {
|
|
228
|
+
throw new Error('Invalid input: empty or non-string value');
|
|
229
|
+
}
|
|
230
|
+
// Check for potentially malicious patterns
|
|
231
|
+
const dangerousPatterns = [
|
|
232
|
+
/<script/i,
|
|
233
|
+
/javascript:/i,
|
|
234
|
+
/on\w+\s*=/i,
|
|
235
|
+
/eval\s*\(/i,
|
|
236
|
+
/Function\s*\(/i,
|
|
237
|
+
];
|
|
238
|
+
for (const pattern of dangerousPatterns) {
|
|
239
|
+
if (pattern.test(trimmed)) {
|
|
240
|
+
throw new Error('Potentially malicious content detected in JSON');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Try to extract JSON from markdown code blocks
|
|
244
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
245
|
+
let jsonStr = fenceMatch ? fenceMatch[1].trim() : trimmed;
|
|
246
|
+
// Remove any leading/trailing text that's not JSON
|
|
247
|
+
const jsonStart = jsonStr.indexOf('{');
|
|
248
|
+
const jsonEnd = jsonStr.lastIndexOf('}');
|
|
249
|
+
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
|
|
250
|
+
jsonStr = jsonStr.substring(jsonStart, jsonEnd + 1);
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(jsonStr);
|
|
254
|
+
// Validate parsed object is not a function or contains functions
|
|
255
|
+
if (typeof parsed === 'function') {
|
|
256
|
+
throw new Error('Invalid JSON: function detected');
|
|
257
|
+
}
|
|
258
|
+
// Recursively check for functions in objects
|
|
259
|
+
const checkForFunctions = (obj) => {
|
|
260
|
+
if (typeof obj === 'function') {
|
|
261
|
+
throw new Error('Invalid JSON: function detected in object');
|
|
262
|
+
}
|
|
263
|
+
if (obj && typeof obj === 'object') {
|
|
264
|
+
for (const key in obj) {
|
|
265
|
+
if (obj.hasOwnProperty(key)) {
|
|
266
|
+
checkForFunctions(obj[key]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
checkForFunctions(parsed);
|
|
272
|
+
return parsed;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
// Log initial parse error
|
|
276
|
+
if (error instanceof Error) {
|
|
277
|
+
console.error(`[JSON Parse] Initial parse failed: ${error.message}`);
|
|
278
|
+
}
|
|
279
|
+
// Try to fix common JSON issues
|
|
280
|
+
try {
|
|
281
|
+
// Fix unescaped quotes in strings
|
|
282
|
+
const fixed = jsonStr
|
|
283
|
+
.replace(/([^\\])"([^"]*)":/g, '$1\\"$2":') // Fix keys
|
|
284
|
+
.replace(/: "([^"]*)"([^,}\]])/g, ': "$1\\"$2'); // Fix values
|
|
285
|
+
const parsed = JSON.parse(fixed);
|
|
286
|
+
// Validate parsed object
|
|
287
|
+
if (typeof parsed === 'function') {
|
|
288
|
+
throw new Error('Invalid JSON: function detected');
|
|
289
|
+
}
|
|
290
|
+
const checkForFunctions = (obj) => {
|
|
291
|
+
if (typeof obj === 'function') {
|
|
292
|
+
throw new Error('Invalid JSON: function detected in object');
|
|
293
|
+
}
|
|
294
|
+
if (obj && typeof obj === 'object') {
|
|
295
|
+
for (const key in obj) {
|
|
296
|
+
if (obj.hasOwnProperty(key)) {
|
|
297
|
+
checkForFunctions(obj[key]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
checkForFunctions(parsed);
|
|
303
|
+
return parsed;
|
|
304
|
+
}
|
|
305
|
+
catch (fixError) {
|
|
306
|
+
console.error('[JSON Parse] Failed to parse JSON:', jsonStr.substring(0, 500));
|
|
307
|
+
console.error('[JSON Parse] Fix attempt also failed:', fixError);
|
|
308
|
+
throw new Error(`Invalid JSON response from AI: ${error instanceof Error ? error.message : 'Parse error'}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|