@bobmatnyc/ai-code-review
Version:
A TypeScript-based tool for automated code reviews using AI models from Google Gemini, Anthropic Claude, and OpenRouter
1,430 lines (1,417 loc) • 1.29 MB
JavaScript
#!/usr/bin/env node
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/utils/logger.ts
function getCurrentLogLevel() {
const shouldLog = process.argv.includes("--trace-logger") && !isInitializing;
if (shouldLog) {
console.error(`Debug: getCurrentLogLevel called, AI_CODE_REVIEW_LOG_LEVEL=${process.env.AI_CODE_REVIEW_LOG_LEVEL}`);
}
if (process.argv.includes("--debug")) {
if (shouldLog) {
console.error("Debug: Debug flag found in process.argv, forcing DEBUG level");
}
return 0 /* DEBUG */;
}
const envLogLevel = process.env.AI_CODE_REVIEW_LOG_LEVEL?.toLowerCase();
if (envLogLevel) {
if (shouldLog) {
console.error(`Debug: Found AI_CODE_REVIEW_LOG_LEVEL environment variable: ${envLogLevel}`);
}
if (envLogLevel in LOG_LEVEL_MAP) {
if (shouldLog) {
console.error(`Debug: Mapped log level ${envLogLevel} -> ${LOG_LEVEL_MAP[envLogLevel]}`);
}
return LOG_LEVEL_MAP[envLogLevel];
} else if (shouldLog) {
console.error(`Debug: Invalid log level: ${envLogLevel}, valid options are: ${Object.keys(LOG_LEVEL_MAP).join(", ")}`);
}
} else if (shouldLog) {
console.error("Debug: AI_CODE_REVIEW_LOG_LEVEL environment variable not found");
}
if (shouldLog) {
console.error("Debug: No valid log level found, defaulting to INFO");
}
return 1 /* INFO */;
}
function setLogLevel(level) {
const shouldLog = process.argv.includes("--trace-logger");
if (shouldLog) {
console.error(`Debug: setLogLevel called with ${level}`);
}
if (typeof level === "string") {
const levelLower = level.toLowerCase();
if (levelLower in LOG_LEVEL_MAP) {
currentLogLevel = LOG_LEVEL_MAP[levelLower];
if (shouldLog) {
console.error(`Debug: Log level set to ${levelLower} -> ${currentLogLevel}`);
}
} else {
console.warn(`Invalid log level: ${level}. Using default.`);
}
} else {
currentLogLevel = level;
if (shouldLog) {
console.error(`Debug: Log level set to numeric value ${level}`);
}
}
}
function getLogLevel() {
return currentLogLevel;
}
function formatLogMessage(level, message) {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const levelUpper = level.toUpperCase().padEnd(5);
return `${COLORS.time}[${timestamp}]${COLORS.reset} ${COLORS[level]}${levelUpper}${COLORS.reset} ${message}`;
}
function log(level, levelName, message, ...args) {
if (level >= currentLogLevel) {
const formattedMessage = formatLogMessage(levelName, message);
switch (level) {
case 0 /* DEBUG */:
console.debug(formattedMessage, ...args);
break;
case 1 /* INFO */:
console.log(formattedMessage, ...args);
break;
case 2 /* WARN */:
console.warn(formattedMessage, ...args);
break;
case 3 /* ERROR */:
console.error(formattedMessage, ...args);
break;
}
} else if (level === 0 /* DEBUG */ && process.argv.includes("--trace-logger")) {
console.error(`Suppressing DEBUG log because currentLogLevel=${currentLogLevel}, message was: ${message}`);
}
}
function debug(message, ...args) {
log(0 /* DEBUG */, "debug", message, ...args);
}
function info(message, ...args) {
log(1 /* INFO */, "info", message, ...args);
}
function warn(message, ...args) {
log(2 /* WARN */, "warn", message, ...args);
}
function error(message, ...args) {
log(3 /* ERROR */, "error", message, ...args);
}
function createLogger(prefix) {
return {
debug: (message, ...args) => debug(`[${prefix}] ${message}`, ...args),
info: (message, ...args) => info(`[${prefix}] ${message}`, ...args),
warn: (message, ...args) => warn(`[${prefix}] ${message}`, ...args),
error: (message, ...args) => error(`[${prefix}] ${message}`, ...args)
};
}
var LogLevel, LOG_LEVEL_MAP, COLORS, isInitializing, currentLogLevel, logger_default;
var init_logger = __esm({
"src/utils/logger.ts"() {
"use strict";
LogLevel = /* @__PURE__ */ ((LogLevel3) => {
LogLevel3[LogLevel3["DEBUG"] = 0] = "DEBUG";
LogLevel3[LogLevel3["INFO"] = 1] = "INFO";
LogLevel3[LogLevel3["WARN"] = 2] = "WARN";
LogLevel3[LogLevel3["ERROR"] = 3] = "ERROR";
LogLevel3[LogLevel3["NONE"] = 4] = "NONE";
return LogLevel3;
})(LogLevel || {});
LOG_LEVEL_MAP = {
debug: 0 /* DEBUG */,
info: 1 /* INFO */,
warn: 2 /* WARN */,
error: 3 /* ERROR */,
none: 4 /* NONE */
};
COLORS = {
reset: "\x1B[0m",
dim: "\x1B[2m",
bright: "\x1B[1m",
debug: "\x1B[36m",
// Cyan
info: "\x1B[32m",
// Green
warn: "\x1B[33m",
// Yellow
error: "\x1B[31m",
// Red
time: "\x1B[90m"
// Gray
};
isInitializing = false;
currentLogLevel = getCurrentLogLevel();
logger_default = {
debug,
info,
warn,
error,
setLogLevel,
getLogLevel,
createLogger,
LogLevel
};
}
});
// src/utils/envLoader.ts
var envLoader_exports = {};
__export(envLoader_exports, {
getAnthropicApiKey: () => getAnthropicApiKey,
getGoogleApiKey: () => getGoogleApiKey,
getOpenAIApiKey: () => getOpenAIApiKey,
getOpenRouterApiKey: () => getOpenRouterApiKey,
loadEnvVariables: () => loadEnvVariables,
validateRequiredEnvVars: () => validateRequiredEnvVars
});
function debugLog(message) {
if (process.argv.includes("--debug") || process.env.AI_CODE_REVIEW_LOG_LEVEL?.toLowerCase() === "debug") {
console.log(`\x1B[36m[DEBUG:ENV]\x1B[0m ${message}`);
}
}
function traceEnvVarLoading(message) {
console.log(`\x1B[35m[ENV-TRACE]\x1B[0m ${message}`);
}
async function loadEnvVariables(envFilePath) {
try {
let envLocalPath;
if (envFilePath) {
envLocalPath = envFilePath;
} else {
const projectEnvLocal = path.resolve(process.cwd(), ".env.local");
const projectEnv = path.resolve(process.cwd(), ".env");
try {
await import_promises.default.access(projectEnvLocal);
envLocalPath = projectEnvLocal;
debugLog(`Found project-level .env.local: ${projectEnvLocal}`);
} catch {
try {
await import_promises.default.access(projectEnv);
envLocalPath = projectEnv;
debugLog(`Found project-level .env: ${projectEnv}`);
} catch {
const possibleToolDirectories2 = [
path.resolve(__dirname, "..", ".."),
// Local development or npm link
path.resolve(__dirname, "..", "..", ".."),
// Global npm installation
"/opt/homebrew/lib/node_modules/@bobmatnyc/ai-code-review"
// Homebrew global installation
];
if (process.env.AI_CODE_REVIEW_DIR) {
possibleToolDirectories2.unshift(process.env.AI_CODE_REVIEW_DIR);
debugLog(`Using tool directory from AI_CODE_REVIEW_DIR: ${process.env.AI_CODE_REVIEW_DIR}`);
}
envLocalPath = projectEnvLocal;
for (const dir of possibleToolDirectories2) {
const potentialEnvPath = path.resolve(dir, ".env.local");
debugLog(`Checking for tool .env.local in: ${potentialEnvPath}`);
try {
await import_promises.default.access(potentialEnvPath);
envLocalPath = potentialEnvPath;
debugLog(`Found .env.local in tool directory: ${potentialEnvPath}`);
break;
} catch (statError) {
debugLog(`No .env.local in ${potentialEnvPath}`);
}
}
}
}
}
try {
await import_promises.default.access(envLocalPath);
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
traceEnvVarLoading(`Environment file not found: ${envLocalPath} (${errorMessage}). Continuing without it.`);
return {
success: true,
message: `No .env.local file found. You may need to set API keys via environment variables or command line options.`,
envFile: envLocalPath
};
}
traceEnvVarLoading(`Attempting to load environment variables from: ${envLocalPath}`);
const beforeModel = process.env.AI_CODE_REVIEW_MODEL;
const result = dotenv.config({ path: envLocalPath, override: true });
if (beforeModel !== process.env.AI_CODE_REVIEW_MODEL) {
traceEnvVarLoading(`AI_CODE_REVIEW_MODEL changed from '${beforeModel}' to '${process.env.AI_CODE_REVIEW_MODEL}'`);
}
if (result.error) {
traceEnvVarLoading(`Error loading environment variables: ${result.error.message}`);
return {
success: false,
message: `Error loading environment variables: ${result.error.message}`,
envFile: envLocalPath
};
}
traceEnvVarLoading(`Successfully loaded environment variables from ${envLocalPath}`);
const envVarNames = Object.keys(result.parsed || {});
if (envVarNames.length > 0) {
traceEnvVarLoading("Variables found in .env.local (names only):");
if (envVarNames.includes("AI_CODE_REVIEW_LOG_LEVEL")) {
traceEnvVarLoading(`AI_CODE_REVIEW_LOG_LEVEL is set to: ${process.env.AI_CODE_REVIEW_LOG_LEVEL}`);
} else {
traceEnvVarLoading("AI_CODE_REVIEW_LOG_LEVEL is NOT present in .env.local");
}
debugLog(envVarNames.join(", "));
} else {
traceEnvVarLoading("No variables found in .env.local");
}
return {
success: true,
message: `Successfully loaded environment variables from ${envLocalPath}`,
envFile: envLocalPath
};
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
console.error(`Error loading environment variables: ${errorMessage}`);
return {
success: false,
message: `Unexpected error loading environment variables: ${errorMessage}`,
envFile: envFilePath
};
}
}
function getGoogleApiKey() {
const apiKeyNew = process.env.AI_CODE_REVIEW_GOOGLE_API_KEY;
const apiKeyLegacy = process.env.CODE_REVIEW_GOOGLE_API_KEY;
const apiKeyGenAI = process.env.GOOGLE_GENERATIVE_AI_KEY;
const apiKeyStudio = process.env.GOOGLE_AI_STUDIO_KEY;
if (apiKeyNew) {
debugLog("Google API key found: AI_CODE_REVIEW_GOOGLE_API_KEY");
return {
apiKey: apiKeyNew,
source: "AI_CODE_REVIEW_GOOGLE_API_KEY",
message: "Using AI_CODE_REVIEW_GOOGLE_API_KEY"
};
}
if (apiKeyLegacy) {
console.warn(
"Warning: Using deprecated environment variable CODE_REVIEW_GOOGLE_API_KEY. Please switch to AI_CODE_REVIEW_GOOGLE_API_KEY."
);
debugLog("Google API key found: CODE_REVIEW_GOOGLE_API_KEY (deprecated)");
return {
apiKey: apiKeyLegacy,
source: "CODE_REVIEW_GOOGLE_API_KEY",
message: "Using deprecated CODE_REVIEW_GOOGLE_API_KEY"
};
}
if (apiKeyGenAI) {
console.warn(
"Warning: Using generic environment variable GOOGLE_GENERATIVE_AI_KEY. Consider using AI_CODE_REVIEW_GOOGLE_API_KEY for better isolation."
);
debugLog("Google API key found: GOOGLE_GENERATIVE_AI_KEY");
return {
apiKey: apiKeyGenAI,
source: "GOOGLE_GENERATIVE_AI_KEY",
message: "Using GOOGLE_GENERATIVE_AI_KEY"
};
}
if (apiKeyStudio) {
console.warn(
"Warning: Using deprecated environment variable GOOGLE_AI_STUDIO_KEY. Please switch to AI_CODE_REVIEW_GOOGLE_API_KEY."
);
debugLog("Google API key found: GOOGLE_AI_STUDIO_KEY (deprecated)");
return {
apiKey: apiKeyStudio,
source: "GOOGLE_AI_STUDIO_KEY",
message: "Using deprecated GOOGLE_AI_STUDIO_KEY"
};
}
return {
apiKey: void 0,
source: "none",
message: "No Google API key found. Please set AI_CODE_REVIEW_GOOGLE_API_KEY in your .env.local file."
};
}
function getOpenRouterApiKey() {
const apiKeyNew = process.env.AI_CODE_REVIEW_OPENROUTER_API_KEY;
const apiKeyLegacy = process.env.CODE_REVIEW_OPENROUTER_API_KEY;
const apiKeyGeneric = process.env.OPENROUTER_API_KEY;
if (apiKeyNew) {
debugLog("OpenRouter API key found: AI_CODE_REVIEW_OPENROUTER_API_KEY");
return {
apiKey: apiKeyNew,
source: "AI_CODE_REVIEW_OPENROUTER_API_KEY",
message: "Using AI_CODE_REVIEW_OPENROUTER_API_KEY"
};
}
if (apiKeyLegacy) {
console.warn(
"Warning: Using deprecated environment variable CODE_REVIEW_OPENROUTER_API_KEY. Please switch to AI_CODE_REVIEW_OPENROUTER_API_KEY."
);
debugLog(
"OpenRouter API key found: CODE_REVIEW_OPENROUTER_API_KEY (deprecated)"
);
return {
apiKey: apiKeyLegacy,
source: "CODE_REVIEW_OPENROUTER_API_KEY",
message: "Using deprecated CODE_REVIEW_OPENROUTER_API_KEY"
};
}
if (apiKeyGeneric) {
console.warn(
"Warning: Using generic environment variable OPENROUTER_API_KEY. Consider using AI_CODE_REVIEW_OPENROUTER_API_KEY for better isolation."
);
debugLog("OpenRouter API key found: OPENROUTER_API_KEY");
return {
apiKey: apiKeyGeneric,
source: "OPENROUTER_API_KEY",
message: "Using OPENROUTER_API_KEY"
};
}
return {
apiKey: void 0,
source: "none",
message: "No OpenRouter API key found. Please set AI_CODE_REVIEW_OPENROUTER_API_KEY in your .env.local file."
};
}
function getAnthropicApiKey() {
const apiKeyNew = process.env.AI_CODE_REVIEW_ANTHROPIC_API_KEY;
const apiKeyLegacy = process.env.CODE_REVIEW_ANTHROPIC_API_KEY;
const apiKeyGeneric = process.env.ANTHROPIC_API_KEY;
if (apiKeyNew) {
debugLog("Anthropic API key found: AI_CODE_REVIEW_ANTHROPIC_API_KEY");
return {
apiKey: apiKeyNew,
source: "AI_CODE_REVIEW_ANTHROPIC_API_KEY",
message: "Using AI_CODE_REVIEW_ANTHROPIC_API_KEY"
};
}
if (apiKeyLegacy) {
console.warn(
"Warning: Using deprecated environment variable CODE_REVIEW_ANTHROPIC_API_KEY. Please switch to AI_CODE_REVIEW_ANTHROPIC_API_KEY."
);
debugLog(
"Anthropic API key found: CODE_REVIEW_ANTHROPIC_API_KEY (deprecated)"
);
return {
apiKey: apiKeyLegacy,
source: "CODE_REVIEW_ANTHROPIC_API_KEY",
message: "Using deprecated CODE_REVIEW_ANTHROPIC_API_KEY"
};
}
if (apiKeyGeneric) {
console.warn(
"Warning: Using generic environment variable ANTHROPIC_API_KEY. Consider using AI_CODE_REVIEW_ANTHROPIC_API_KEY for better isolation."
);
debugLog("Anthropic API key found: ANTHROPIC_API_KEY");
return {
apiKey: apiKeyGeneric,
source: "ANTHROPIC_API_KEY",
message: "Using ANTHROPIC_API_KEY"
};
}
return {
apiKey: void 0,
source: "none",
message: "No Anthropic API key found. Please set AI_CODE_REVIEW_ANTHROPIC_API_KEY in your .env.local file."
};
}
function getOpenAIApiKey() {
const apiKeyNew = process.env.AI_CODE_REVIEW_OPENAI_API_KEY;
const apiKeyLegacy = process.env.CODE_REVIEW_OPENAI_API_KEY;
const apiKeyGeneric = process.env.OPENAI_API_KEY;
if (apiKeyNew) {
debugLog("OpenAI API key found: AI_CODE_REVIEW_OPENAI_API_KEY");
return {
apiKey: apiKeyNew,
source: "AI_CODE_REVIEW_OPENAI_API_KEY",
message: "Using AI_CODE_REVIEW_OPENAI_API_KEY"
};
}
if (apiKeyLegacy) {
console.warn(
"Warning: Using deprecated environment variable CODE_REVIEW_OPENAI_API_KEY. Please switch to AI_CODE_REVIEW_OPENAI_API_KEY."
);
debugLog("OpenAI API key found: CODE_REVIEW_OPENAI_API_KEY (deprecated)");
return {
apiKey: apiKeyLegacy,
source: "CODE_REVIEW_OPENAI_API_KEY",
message: "Using deprecated CODE_REVIEW_OPENAI_API_KEY"
};
}
if (apiKeyGeneric) {
console.warn(
"Warning: Using generic environment variable OPENAI_API_KEY. Consider using AI_CODE_REVIEW_OPENAI_API_KEY for better isolation."
);
debugLog("OpenAI API key found: OPENAI_API_KEY");
return {
apiKey: apiKeyGeneric,
source: "OPENAI_API_KEY",
message: "Using OPENAI_API_KEY"
};
}
return {
apiKey: void 0,
source: "none",
message: "No OpenAI API key found. Please set AI_CODE_REVIEW_OPENAI_API_KEY in your .env.local file."
};
}
function validateRequiredEnvVars() {
const googleApiKey = getGoogleApiKey();
const openRouterApiKey = getOpenRouterApiKey();
const anthropicApiKey = getAnthropicApiKey();
const openaiApiKey = getOpenAIApiKey();
if (googleApiKey.apiKey || openRouterApiKey.apiKey || anthropicApiKey.apiKey || openaiApiKey.apiKey) {
return {
valid: true,
message: "At least one API key is available"
};
}
return {
valid: false,
message: "No API keys found. Please set either AI_CODE_REVIEW_GOOGLE_API_KEY or AI_CODE_REVIEW_OPENROUTER_API_KEY in your .env.local file."
};
}
var path, dotenv, import_promises;
var init_envLoader = __esm({
"src/utils/envLoader.ts"() {
"use strict";
path = __toESM(require("path"));
dotenv = __toESM(require("dotenv"));
import_promises = __toESM(require("fs/promises"));
}
});
// src/utils/config.ts
var config_exports = {};
__export(config_exports, {
appConfigSchema: () => appConfigSchema,
getApiKeyForProvider: () => getApiKeyForProvider,
getConfig: () => getConfig,
getPromptsPath: () => getPromptsPath,
hasAnyApiKey: () => hasAnyApiKey,
resetConfig: () => resetConfig,
validateConfigForSelectedModel: () => validateConfigForSelectedModel
});
function loadConfig(cliOptions) {
const googleApiKeyResult = getGoogleApiKey();
const openRouterApiKeyResult = getOpenRouterApiKey();
const anthropicApiKeyResult = getAnthropicApiKey();
const openAIApiKeyResult = getOpenAIApiKey();
const googleApiKey = cliOptions?.apiKey?.google || cliOptions?.apiKeys?.google || googleApiKeyResult.apiKey;
const openRouterApiKey = cliOptions?.apiKey?.openrouter || cliOptions?.apiKeys?.openrouter || openRouterApiKeyResult.apiKey;
const anthropicApiKey = cliOptions?.apiKey?.anthropic || cliOptions?.apiKeys?.anthropic || anthropicApiKeyResult.apiKey;
const openAIApiKey = cliOptions?.apiKey?.openai || cliOptions?.apiKeys?.openai || openAIApiKeyResult.apiKey;
const selectedModel = cliOptions?.model || process.env.AI_CODE_REVIEW_MODEL || "gemini:gemini-2.5-pro-preview";
const writerModel = cliOptions?.writerModel || process.env.AI_CODE_REVIEW_WRITER_MODEL || void 0;
const debug2 = cliOptions?.debug || process.env.AI_CODE_REVIEW_DEBUG === "true" || process.argv.includes("--debug");
const logLevel = cliOptions?.logLevel || process.env.AI_CODE_REVIEW_LOG_LEVEL || "info";
const contextPathsStr = process.env.AI_CODE_REVIEW_CONTEXT;
const contextPaths = contextPathsStr ? contextPathsStr.split(",").map((p) => p.trim()) : void 0;
const outputDir = cliOptions?.outputDir || process.env.AI_CODE_REVIEW_OUTPUT_DIR || "ai-code-review-docs";
const configObj = {
googleApiKey,
openRouterApiKey,
anthropicApiKey,
openAIApiKey,
selectedModel,
writerModel,
debug: debug2,
logLevel,
contextPaths,
outputDir
};
try {
return appConfigSchema.parse(configObj);
} catch (error2) {
if (error2 instanceof import_zod.z.ZodError) {
logger_default.error("Configuration validation failed:", error2.errors);
throw new Error(
`Configuration validation failed: ${error2.errors.map((e) => e.message).join(", ")}`
);
}
throw error2;
}
}
function getConfig(cliOptions) {
if (!config2 || cliOptions) {
try {
config2 = loadConfig(cliOptions);
} catch (error2) {
logger_default.error("Failed to load configuration:", error2);
throw error2;
}
}
return config2;
}
function hasAnyApiKey() {
const { googleApiKey, openRouterApiKey, anthropicApiKey, openAIApiKey } = getConfig();
return !!(googleApiKey || openRouterApiKey || anthropicApiKey || openAIApiKey);
}
function getApiKeyForProvider(provider) {
const config4 = getConfig();
switch (provider.toLowerCase()) {
case "gemini":
return config4.googleApiKey;
case "openrouter":
return config4.openRouterApiKey;
case "anthropic":
return config4.anthropicApiKey;
case "openai":
return config4.openAIApiKey;
default:
return void 0;
}
}
function resetConfig() {
config2 = null;
}
function validateConfigForSelectedModel() {
const config4 = getConfig();
const [provider] = config4.selectedModel.split(":");
if (!provider) {
return {
valid: false,
message: `Invalid model format: ${config4.selectedModel}. Expected format: provider:model-name`
};
}
switch (provider.toLowerCase()) {
case "gemini":
if (!config4.googleApiKey) {
return {
valid: false,
message: `Missing Google API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_GOOGLE_API_KEY in your .env.local file.`
};
}
break;
case "openrouter":
if (!config4.openRouterApiKey) {
return {
valid: false,
message: `Missing OpenRouter API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_OPENROUTER_API_KEY in your .env.local file.`
};
}
break;
case "anthropic":
if (!config4.anthropicApiKey) {
return {
valid: false,
message: `Missing Anthropic API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_ANTHROPIC_API_KEY in your .env.local file.`
};
}
break;
case "openai":
if (!config4.openAIApiKey) {
return {
valid: false,
message: `Missing OpenAI API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_OPENAI_API_KEY in your .env.local file.`
};
}
break;
default:
return {
valid: false,
message: `Unknown provider: ${provider}. Supported providers are: gemini, openrouter, anthropic, openai`
};
}
return {
valid: true,
message: "Configuration is valid for the selected model"
};
}
function getPromptsPath() {
const possiblePaths = [
// For local development
import_path.default.resolve("prompts"),
// For npm package
import_path.default.resolve(__dirname, "..", "..", "prompts"),
// For global installation
import_path.default.resolve(__dirname, "..", "..", "..", "prompts")
];
for (const p of possiblePaths) {
if (import_fs.default.existsSync(p)) {
return p;
}
}
return possiblePaths[0];
}
var import_path, import_fs, import_zod, appConfigSchema, config2;
var init_config = __esm({
"src/utils/config.ts"() {
"use strict";
init_envLoader();
init_logger();
import_path = __toESM(require("path"));
import_fs = __toESM(require("fs"));
import_zod = require("zod");
appConfigSchema = import_zod.z.object({
// API Keys
googleApiKey: import_zod.z.string().optional(),
openRouterApiKey: import_zod.z.string().optional(),
anthropicApiKey: import_zod.z.string().optional(),
openAIApiKey: import_zod.z.string().optional(),
// Model configuration
selectedModel: import_zod.z.string(),
writerModel: import_zod.z.string().optional(),
// Other configuration
debug: import_zod.z.boolean(),
logLevel: import_zod.z.enum(["debug", "info", "warn", "error", "none"]).default("info"),
contextPaths: import_zod.z.array(import_zod.z.string()).optional(),
outputDir: import_zod.z.string().default("ai-code-review-docs")
});
config2 = null;
}
});
// src/tokenizers/baseTokenizer.ts
function getTokenizer(modelName) {
const modelNameLower = modelName.toLowerCase();
if (modelNameLower.includes("gpt")) {
return TokenizerRegistry.getAllTokenizers().find(
(t2) => t2.getModelName() === "gpt"
) || new FallbackTokenizer();
}
if (modelNameLower.includes("claude")) {
return TokenizerRegistry.getAllTokenizers().find(
(t2) => t2.getModelName() === "claude"
) || new FallbackTokenizer();
}
if (modelNameLower.includes("gemini")) {
return TokenizerRegistry.getAllTokenizers().find(
(t2) => t2.getModelName() === "gemini"
) || new FallbackTokenizer();
}
return new FallbackTokenizer();
}
function countTokens(text, modelName) {
const tokenizer = getTokenizer(modelName);
const count = tokenizer.countTokens(text);
if (modelName === "test-small-context") {
return text.length;
}
return count;
}
var TokenizerRegistry, FallbackTokenizer;
var init_baseTokenizer = __esm({
"src/tokenizers/baseTokenizer.ts"() {
"use strict";
TokenizerRegistry = class _TokenizerRegistry {
static tokenizers = [];
/**
* Register a tokenizer
* @param tokenizer Tokenizer to register
*/
static register(tokenizer) {
_TokenizerRegistry.tokenizers.push(tokenizer);
}
/**
* Get a tokenizer for a specific model
* @param modelName Name of the model
* @returns Tokenizer for the model, or undefined if none found
*/
static getTokenizer(modelName) {
return _TokenizerRegistry.tokenizers.find((t2) => t2.supportsModel(modelName));
}
/**
* Get all registered tokenizers
* @returns Array of all registered tokenizers
*/
static getAllTokenizers() {
return [..._TokenizerRegistry.tokenizers];
}
};
FallbackTokenizer = class {
/**
* Count the number of tokens in a text using a simple approximation
* @param text Text to count tokens for
* @returns Estimated token count
*/
countTokens(text) {
return Math.ceil(text.length / 4);
}
/**
* Get the model name for this tokenizer
* @returns 'fallback'
*/
getModelName() {
return "fallback";
}
/**
* This tokenizer is used as a fallback for any model
* @param _modelName Name of the model (unused but required by interface)
* @returns Always true
*/
supportsModel(_modelName) {
return true;
}
};
TokenizerRegistry.register(new FallbackTokenizer());
}
});
// src/tokenizers/gptTokenizer.ts
var import_gpt_tokenizer, GPTTokenizer;
var init_gptTokenizer = __esm({
"src/tokenizers/gptTokenizer.ts"() {
"use strict";
import_gpt_tokenizer = require("gpt-tokenizer");
init_baseTokenizer();
GPTTokenizer = class {
modelPatterns = [/gpt/i];
/**
* Count the number of tokens in a text using the GPT tokenizer
* @param text Text to count tokens for
* @returns Actual token count
*/
countTokens(text) {
try {
const tokens = (0, import_gpt_tokenizer.encode)(text);
return tokens.length;
} catch (error2) {
console.warn(`Error counting tokens with GPT tokenizer: ${error2}`);
return Math.ceil(text.length / 4);
}
}
/**
* Get the model name for this tokenizer
* @returns 'gpt'
*/
getModelName() {
return "gpt";
}
/**
* Check if this tokenizer supports a given model
* @param modelName Name of the model to check
* @returns True if the model is supported, false otherwise
*/
supportsModel(modelName) {
const lowerModelName = modelName.toLowerCase();
return this.modelPatterns.some((pattern) => pattern.test(lowerModelName));
}
};
TokenizerRegistry.register(new GPTTokenizer());
}
});
// src/tokenizers/claudeTokenizer.ts
var import_tokenizer, ClaudeTokenizer;
var init_claudeTokenizer = __esm({
"src/tokenizers/claudeTokenizer.ts"() {
"use strict";
import_tokenizer = require("@anthropic-ai/tokenizer");
init_baseTokenizer();
ClaudeTokenizer = class {
modelPatterns = [/claude/i];
/**
* Count the number of tokens in a text using the Claude tokenizer
* @param text Text to count tokens for
* @returns Actual token count
*/
countTokens(text) {
try {
return (0, import_tokenizer.countTokens)(text);
} catch (error2) {
console.warn(`Error counting tokens with Claude tokenizer: ${error2}`);
return Math.ceil(text.length / 4);
}
}
/**
* Get the model name for this tokenizer
* @returns 'claude'
*/
getModelName() {
return "claude";
}
/**
* Check if this tokenizer supports a given model
* @param modelName Name of the model to check
* @returns True if the model is supported, false otherwise
*/
supportsModel(modelName) {
const lowerModelName = modelName.toLowerCase();
return this.modelPatterns.some((pattern) => pattern.test(lowerModelName));
}
};
TokenizerRegistry.register(new ClaudeTokenizer());
}
});
// src/tokenizers/geminiTokenizer.ts
var GeminiTokenizer;
var init_geminiTokenizer = __esm({
"src/tokenizers/geminiTokenizer.ts"() {
"use strict";
init_baseTokenizer();
GeminiTokenizer = class {
modelPatterns = [/gemini/i];
/**
* Count the number of tokens in a text using an approximation for Gemini models
* @param text Text to count tokens for
* @returns Estimated token count
*/
countTokens(text) {
return Math.ceil(text.length / 4);
}
/**
* Get the model name for this tokenizer
* @returns 'gemini'
*/
getModelName() {
return "gemini";
}
/**
* Check if this tokenizer supports a given model
* @param modelName Name of the model to check
* @returns True if the model is supported, false otherwise
*/
supportsModel(modelName) {
const lowerModelName = modelName.toLowerCase();
return this.modelPatterns.some((pattern) => pattern.test(lowerModelName));
}
};
TokenizerRegistry.register(new GeminiTokenizer());
}
});
// src/tokenizers/index.ts
var init_tokenizers = __esm({
"src/tokenizers/index.ts"() {
"use strict";
init_baseTokenizer();
init_gptTokenizer();
init_claudeTokenizer();
init_geminiTokenizer();
}
});
// src/estimators/abstractEstimator.ts
var AbstractTokenEstimator;
var init_abstractEstimator = __esm({
"src/estimators/abstractEstimator.ts"() {
"use strict";
init_tokenizers();
AbstractTokenEstimator = class {
/**
* Estimate the number of tokens in a text
* @param text Text to estimate tokens for
* @param modelName Optional model name to use for tokenization
* @returns Estimated token count
*/
estimateTokenCount(text, modelName) {
return countTokens(text, modelName || this.getDefaultModel());
}
/**
* Format a cost value as a currency string
* @param cost Cost value in USD
* @returns Formatted cost string
*/
formatCost(cost) {
return `$${cost.toFixed(6)} USD`;
}
/**
* Get cost information based on token counts
* @param inputTokens Number of input tokens
* @param outputTokens Number of output tokens
* @param modelName Name of the model (optional)
* @returns Cost information
*/
getCostInfo(inputTokens, outputTokens, modelName) {
const totalTokens = inputTokens + outputTokens;
const estimatedCost = this.calculateCost(
inputTokens,
outputTokens,
modelName
);
return {
inputTokens,
outputTokens,
totalTokens,
estimatedCost,
formattedCost: this.formatCost(estimatedCost)
};
}
/**
* Get cost information based on text
* @param inputText Input text
* @param outputText Output text
* @param modelName Name of the model (optional)
* @returns Cost information
*/
getCostInfoFromText(inputText, outputText, modelName) {
const model = modelName || this.getDefaultModel();
const inputTokens = this.estimateTokenCount(inputText, model);
const outputTokens = this.estimateTokenCount(outputText, model);
return this.getCostInfo(inputTokens, outputTokens, model);
}
};
}
});
// src/estimators/geminiEstimator.ts
var GeminiTokenEstimator;
var init_geminiEstimator = __esm({
"src/estimators/geminiEstimator.ts"() {
"use strict";
init_abstractEstimator();
GeminiTokenEstimator = class _GeminiTokenEstimator extends AbstractTokenEstimator {
static instance;
/**
* Get the singleton instance of the estimator
* @returns GeminiTokenEstimator instance
*/
static getInstance() {
if (!_GeminiTokenEstimator.instance) {
_GeminiTokenEstimator.instance = new _GeminiTokenEstimator();
}
return _GeminiTokenEstimator.instance;
}
/**
* Pricing information for Gemini models
*/
MODEL_PRICING = {
// Gemini 2.5 models
"gemini-2.5-pro": {
type: "tiered",
tiers: [
{
threshold: 0,
inputTokenCost: 125e-5,
// $1.25 per 1M tokens (≤200k tokens)
outputTokenCost: 0.01
// $10.00 per 1M tokens (≤200k tokens)
},
{
threshold: 2e5,
inputTokenCost: 25e-4,
// $2.50 per 1M tokens (>200k tokens)
outputTokenCost: 0.015
// $15.00 per 1M tokens (>200k tokens)
}
]
},
"gemini-2.5-pro-preview": {
type: "tiered",
tiers: [
{
threshold: 0,
inputTokenCost: 125e-5,
// $1.25 per 1M tokens (≤200k tokens)
outputTokenCost: 0.01
// $10.00 per 1M tokens (≤200k tokens)
},
{
threshold: 2e5,
inputTokenCost: 25e-4,
// $2.50 per 1M tokens (>200k tokens)
outputTokenCost: 0.015
// $15.00 per 1M tokens (>200k tokens)
}
]
},
"gemini-2.5-pro-exp": {
type: "tiered",
tiers: [
{
threshold: 0,
inputTokenCost: 125e-5,
// $1.25 per 1M tokens (≤200k tokens)
outputTokenCost: 0.01
// $10.00 per 1M tokens (≤200k tokens)
},
{
threshold: 2e5,
inputTokenCost: 25e-4,
// $2.50 per 1M tokens (>200k tokens)
outputTokenCost: 0.015
// $15.00 per 1M tokens (>200k tokens)
}
]
},
"gemini-2.0-flash": {
type: "standard",
inputTokenCost: 1e-4,
// $0.10 per 1M tokens
outputTokenCost: 4e-4
// $0.40 per 1M tokens
},
"gemini-2.0-flash-lite": {
type: "standard",
inputTokenCost: 75e-6,
// $0.075 per 1M tokens
outputTokenCost: 3e-4
// $0.30 per 1M tokens
},
// Gemini 1.5 models
"gemini-1.5-pro": {
type: "tiered",
tiers: [
{
threshold: 0,
inputTokenCost: 125e-5,
// $1.25 per 1M tokens (≤128k tokens)
outputTokenCost: 5e-3
// $5.00 per 1M tokens (≤128k tokens)
},
{
threshold: 128e3,
inputTokenCost: 25e-4,
// $2.50 per 1M tokens (>128k tokens)
outputTokenCost: 0.01
// $10.00 per 1M tokens (>128k tokens)
}
]
},
"gemini-1.5-flash": {
type: "tiered",
tiers: [
{
threshold: 0,
inputTokenCost: 75e-6,
// $0.075 per 1M tokens (≤128k tokens)
outputTokenCost: 3e-4
// $0.30 per 1M tokens (≤128k tokens)
},
{
threshold: 128e3,
inputTokenCost: 15e-5,
// $0.15 per 1M tokens (>128k tokens)
outputTokenCost: 6e-4
// $0.60 per 1M tokens (>128k tokens)
}
]
},
"gemini-1.5-flash-8b": {
type: "tiered",
tiers: [
{
threshold: 0,
inputTokenCost: 375e-7,
// $0.0375 per 1M tokens (≤128k tokens)
outputTokenCost: 15e-5
// $0.15 per 1M tokens (≤128k tokens)
},
{
threshold: 128e3,
inputTokenCost: 75e-6,
// $0.075 per 1M tokens (>128k tokens)
outputTokenCost: 3e-4
// $0.30 per 1M tokens (>128k tokens)
}
]
},
// Default fallback pricing
default: {
type: "standard",
inputTokenCost: 1e-3,
// $1.00 per 1M tokens
outputTokenCost: 2e-3
// $2.00 per 1M tokens
}
};
/**
* Private constructor to enforce singleton pattern
*/
constructor() {
super();
}
/**
* Get the pricing for a specific model
* @param modelName Name of the model
* @returns Pricing information for the model
*/
getModelPricing(modelName) {
return this.MODEL_PRICING[modelName] || this.MODEL_PRICING["default"];
}
/**
* Calculate the cost for a specific tier
* @param tokens Number of tokens
* @param tokenCost Cost per 1K tokens
* @param tierStart Start of the tier
* @param tierEnd End of the tier (or undefined for no upper limit)
* @returns Cost for this tier
*/
calculateTierCost(tokens, tokenCost, tierStart, tierEnd) {
const tierTokens = tierEnd ? Math.min(Math.max(0, tokens - tierStart), tierEnd - tierStart) : Math.max(0, tokens - tierStart);
return tierTokens / 1e3 * tokenCost;
}
/**
* Calculate the cost for a given number of input and output tokens
* @param inputTokens Number of input tokens
* @param outputTokens Number of output tokens
* @param modelName Name of the model (optional, uses default if not provided)
* @returns Estimated cost in USD
*/
calculateCost(inputTokens, outputTokens, modelName = this.getDefaultModel()) {
const pricing = this.getModelPricing(modelName);
let inputCost = 0;
let outputCost = 0;
if (pricing.type === "standard") {
inputCost = inputTokens / 1e3 * pricing.inputTokenCost;
outputCost = outputTokens / 1e3 * pricing.outputTokenCost;
} else if (pricing.type === "tiered") {
const tiers = pricing.tiers;
for (let i = 0; i < tiers.length; i++) {
const tierStart = tiers[i].threshold;
const tierEnd = i < tiers.length - 1 ? tiers[i + 1].threshold : void 0;
inputCost += this.calculateTierCost(
inputTokens,
tiers[i].inputTokenCost,
tierStart,
tierEnd
);
}
for (let i = 0; i < tiers.length; i++) {
const tierStart = tiers[i].threshold;
const tierEnd = i < tiers.length - 1 ? tiers[i + 1].threshold : void 0;
outputCost += this.calculateTierCost(
outputTokens,
tiers[i].outputTokenCost,
tierStart,
tierEnd
);
}
}
return inputCost + outputCost;
}
/**
* Get the default model name for this estimator
* @returns Default model name
*/
getDefaultModel() {
return "gemini-1.5-pro";
}
/**
* Check if this estimator supports a given model
* @param modelName Name of the model to check
* @returns True if the model is supported, false otherwise
*/
supportsModel(modelName) {
return modelName in this.MODEL_PRICING || modelName.startsWith("gemini-");
}
};
}
});
// src/clients/utils/modelMaps/types.ts
var ModelCategory;
var init_types = __esm({
"src/clients/utils/modelMaps/types.ts"() {
"use strict";
ModelCategory = /* @__PURE__ */ ((ModelCategory2) => {
ModelCategory2["REASONING"] = "reasoning";
ModelCategory2["FAST_INFERENCE"] = "fast-inference";
ModelCategory2["COST_OPTIMIZED"] = "cost-optimized";
ModelCategory2["LONG_CONTEXT"] = "long-context";
ModelCategory2["MULTIMODAL"] = "multimodal";
ModelCategory2["CODING"] = "coding";
return ModelCategory2;
})(ModelCategory || {});
}
});
// src/clients/utils/modelMaps/data/gemini.ts
var GEMINI_MODELS;
var init_gemini = __esm({
"src/clients/utils/modelMaps/data/gemini.ts"() {
"use strict";
init_types();
GEMINI_MODELS = {
"gemini:gemini-2.5-pro-preview": {
apiIdentifier: "gemini-2.5-pro-preview-05-06",
displayName: "Gemini 2.5 Pro Preview",
provider: "gemini",
useV1Beta: true,
contextWindow: 1e6,
description: "Most advanced reasoning and multimodal capabilities",
apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY",
supportsToolCalling: false,
status: "preview",
categories: ["reasoning" /* REASONING */, "long-context" /* LONG_CONTEXT */, "multimodal" /* MULTIMODAL */],
capabilities: ["advanced-reasoning", "multimodal", "code-generation", "long-context"],
tieredPricing: [
{ tokenThreshold: 0, inputPricePerMillion: 1.25, outputPricePerMillion: 5 },
{ tokenThreshold: 2e5, inputPricePerMillion: 2.5, outputPricePerMillion: 10 }
],
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
toolCallingSupport: "partial"
}
},
"gemini:gemini-2.5-pro": {
apiIdentifier: "gemini-2.5-pro-preview-05-06",
displayName: "Gemini 2.5 Pro",
provider: "gemini",
useV1Beta: true,
contextWindow: 1e6,
description: "Production-ready advanced reasoning model",
apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY",
supportsToolCalling: false,
status: "available",
categories: ["reasoning" /* REASONING */, "long-context" /* LONG_CONTEXT */, "multimodal" /* MULTIMODAL */],
capabilities: ["advanced-reasoning", "multimodal", "code-generation", "long-context"],
tieredPricing: [
{ tokenThreshold: 0, inputPricePerMillion: 1.25, outputPricePerMillion: 5 },
{ tokenThreshold: 2e5, inputPricePerMillion: 2.5, outputPricePerMillion: 10 }
],
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
toolCallingSupport: "partial"
}
},
"gemini:gemini-2.0-flash-lite": {
apiIdentifier: "gemini-2.0-flash-lite",
displayName: "Gemini 2.0 Flash Lite",
provider: "gemini",
useV1Beta: true,
contextWindow: 1e6,
description: "Ultra-fast, cost-efficient model for simple tasks",
apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY",
supportsToolCalling: false,
status: "available",
categories: ["fast-inference" /* FAST_INFERENCE */, "cost-optimized" /* COST_OPTIMIZED */],
capabilities: ["fast-inference", "basic-reasoning"],
inputPricePerMillion: 0.05,
outputPricePerMillion: 0.15,
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
toolCallingSupport: "none"
}
},
"gemini:gemini-2.0-flash": {
apiIdentifier: "gemini-2.0-flash-preview-05-07",
displayName: "Gemini 2.0 Flash",
provider: "gemini",
useV1Beta: true,
contextWindow: 1e6,
description: "Fast, efficient model with strong performance",
apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY",
supportsToolCalling: false,
status: "preview",
categories: ["fast-inference" /* FAST_INFERENCE */, "long-context" /* LONG_CONTEXT */],
capabilities: ["fast-inference", "good-reasoning", "long-context"],
inputPricePerMillion: 0.3,
outputPricePerMillion: 1.2,
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
toolCallingSupport: "partial"
}
},
"gemini:gemini-1.5-pro": {
apiIdentifier: "gemini-1.5-pro",
displayName: "Gemini 1.5 Pro",
provider: "gemini",
useV1Beta: false,
contextWindow: 1e6,
description: "Previous generation large context model",
apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY",
supportsToolCalling: false,
status: "available",
categories: ["long-context" /* LONG_CONTEXT */],
capabilities: ["long-context", "good-reasoning"],
tieredPricing: [
{ tokenThreshold: 0, inputPricePerMillion: 1.25, outputPricePerMillion: 5 },
{ tokenThreshold: 128e3, inputPricePerMillion: 2.5, outputPricePerMillion: 10 }
],
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
toolCallingSupport: "partial"
}
},
"gemini:gemini-1.5-flash": {
apiIdentifier: "gemini-1.5-flash",
displayName: "Gemini 1.5 Flash",
provider: "gemini",
useV1Beta: false,
contextWindow: 1e6,
description: "Previous generation fast model",
apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY",
supportsToolCalling: false,
status: "available",
categories: ["fast-inference" /* FAST_INFERENCE */, "long-context" /* LONG_CONTEXT */],
capabilities: ["fast-inference", "long-context"],
tieredPricing: [
{ tokenThreshold: 0, inputPricePerMillion: 0.075, outputPricePerMillion: 0.3 },
{ tokenThreshold: 128e3, inputPricePerMillion: 0.15, outputPricePerMillion: 0.6 }
],
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
toolCallingSupport: "partial"
}
}
};
}
});
// src/clients/utils/modelMaps/data/anthropic.ts
var ANTHROPIC_MODELS;
var init_anthropic = __esm({
"src/clients/utils/modelMaps/data/anthropic.ts"() {
"use strict";
init_types();
ANTHROPIC_MODELS = {
"anthropic:claude-4-opus": {
apiIdentifier: "claude-4-opus-20241022",
displayName: "Claude 4 Opus",
provider: "anthropic",
contextWindow: 2e5,
description: "Most capable Claude model with superior reasoning",
apiKeyEnvVar: "AI_CODE_REVIEW_ANTHROPIC_API_KEY",
supportsToolCalling: true,
status: "available",
categories: ["reasoning" /* REASONING */, "coding" /* CODING */],
capabilities: ["advanced-reasoning", "code-generation", "code-review", "analysis"],
inputPricePerMillion: 15,
outputPricePerMillion: 75,
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
supportsPromptCaching: true,
toolCallingSupport: "full"
}
},
"anthropic:claude-4-sonnet": {
apiIdentifier: "claude-4-sonnet-20241022",
displayName: "Claude 4 Sonnet",
provider: "anthropic",
contextWindow: 2e5,
description: "Balanced performance and cost for code review",
apiKeyEnvVar: "AI_CODE_REVIEW_ANTHROPIC_API_KEY",
supportsToolCalling: true,
status: "available",
categories: ["reasoning" /* REASONING */, "coding" /* CODING */, "cost-optimized" /* COST_OPTIMIZED */],
capabilities: ["good-reasoning", "code-generation", "code-review"],
inputPricePerMillion: 3,
outputPricePerMillion: 15,
providerFeatures: {
supportsStreaming: true,
supportsBatch: true,
supportsPromptCaching: true,
toolCallingSupport: "full"
},
notes: "Recommended model for code review tasks"
},
"anthropic:claude-3.5-sonnet": {
apiIdentifier: "claude-3-5-sonnet-20241022",
displayName: "Claude 3.5 Sonnet",
provider: "anthropic",
contextWindow: 2e5,
description: "Enhanced Claude 3 with improved capabilities",
apiKeyEnvVar: "AI_CODE_REVIEW_ANTHROPIC_API_KEY",
supportsToolCalling: true,
st