diff --git a/bun.lockb b/bun.lockb index 3b16506c..058c590a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7d267f16..12d847a1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,10 @@ "huggingface", "ai", "genai", - "llama" + "llama", + "deepseek", + "deepseek-chat", + "deepseek-R1" ], "author": "Matt Carey", "license": "MIT", @@ -55,7 +58,8 @@ "tinyglobby": "^0.2.10", "tslog": "^4.8.2", "yargs": "^17.7.2", - "zod": "^3.24.1" + "zod": "^3.24.1", + "openai": "^4.79.4" }, "devDependencies": { "@biomejs/biome": "^1.9.4", @@ -66,7 +70,5 @@ "npm-dts": "^1.3.12", "typescript": "^5.1.6" }, - "files": [ - "dist/*" - ] + "files": ["dist/*"] } diff --git a/setup.md b/setup.md index 6199ec94..647b78a0 100644 --- a/setup.md +++ b/setup.md @@ -103,7 +103,7 @@ You can now run `code-review-gpt review` in the root directory of any git-enable ### Options -- `--ci` - Used with the `review` command. Options are --ci=("github" | "gitlab"). Defaults to "github" if no option is specified. Runs the application in CI mode. This will use the BASE_SHA and GITHUB_SHA environment variables to determine which files to review. It will also use the GITHUB_TOKEN environment variable to create a comment on the pull request with the review results. +- `--ci` - Used with the `review` command. Options are --ci=("github" | "gitlab" | "azdev"). Defaults to "github" if no option is specified. Runs the application in CI mode. This will use the BASE_SHA and GITHUB_SHA environment variables to determine which files to review. It will also use the GITHUB_TOKEN environment variable to create a comment on the pull request with the review results. - `--reviewType` - Used with the 'review' command. The options are --reviewType=("changed" | "full" | "costOptimized). Defaults to "changed" if no option is specified. Specifies whether the review is for the full file or just the changed lines. costOptimized limits the context surrounding the changed lines to 5 lines. diff --git a/src/args.ts b/src/args.ts index b5a4420b..e4032670 100644 --- a/src/args.ts +++ b/src/args.ts @@ -2,7 +2,7 @@ import rawlist from '@inquirer/rawlist'; import dotenv from 'dotenv'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; - +import { modelsNames } from './review/constants'; import type { ReviewArgs } from './common/types'; dotenv.config(); @@ -22,7 +22,7 @@ const handleNoCommand = async (): Promise => { return command; }; -export const getYargs = async (): Promise => { +export const getYargs = async () => { return yargs(hideBin(process.argv)) .command('configure', 'Configure the tool') .command('review', 'Review code changes') @@ -49,6 +49,7 @@ export const getYargs = async (): Promise => { }) .option('model', { description: 'The model to use for generating the review.', + choices: modelsNames, type: 'string', default: 'gpt-4o-mini', }) @@ -82,10 +83,10 @@ export const getYargs = async (): Promise => { }) .option('provider', { description: 'Provider to use for AI', - choices: ['openai', 'azureai', 'bedrock'], + choices: ['openai', 'azureai', 'bedrock', 'deepseek'], type: 'string', default: 'openai', }) .help() - .parse(); + .parse() as ReviewArgs; }; diff --git a/src/common/model/AIModel.ts b/src/common/model/AIModel.ts index 7d8b2400..35dd6d34 100644 --- a/src/common/model/AIModel.ts +++ b/src/common/model/AIModel.ts @@ -1,20 +1,22 @@ import { ChatOpenAI, AzureChatOpenAI } from '@langchain/openai'; - +import { OpenAI as DeepSeekAI } from 'openai'; import type { ZodType } from 'zod'; import type { IFeedback } from '../types'; import { logger } from '../utils/logger'; +import type { AIModelName } from '../../review/constants'; +import type { ProviderOptions } from '../../common/types'; interface IAIModel { - modelName: string; - provider: string; + modelName: AIModelName; + provider: ProviderOptions; temperature: number; apiKey: string; retryCount?: number; - organization: string | undefined; + organization?: string; } export class AIModel { - private model: ChatOpenAI; + private model: ChatOpenAI | ReturnType; constructor(options: IAIModel) { switch (options.provider) { @@ -26,11 +28,19 @@ export class AIModel { modelName: options.modelName, }); break; - case "azureai": + case 'azureai': this.model = new AzureChatOpenAI({ temperature: options.temperature, }); break; + case 'deepseek': + this.model = createDeepSeekModel({ + apiKey: options.apiKey, + baseURL: 'https://api.deepseek.com', + temperature: options.temperature, + model: options.modelName as Extract, + }); + break; case 'bedrock': throw new Error('Bedrock provider not implemented'); default: @@ -39,25 +49,33 @@ export class AIModel { } public async callModel(prompt: string): Promise { - const message = await this.model.invoke(prompt); - return message.content[0] as string; + if ('callModel' in this.model) { + return this.model.callModel(prompt); + } else { + const message = await this.model.invoke(prompt); + return message.content[0] as string; + } } public async callStructuredModel(prompt: string, schema: ZodType): Promise { - const modelWithStructuredOutput = this.model.withStructuredOutput(schema, { - method: 'jsonSchema', - strict: true, - includeRaw: true, - }); - const res = await modelWithStructuredOutput.invoke(prompt); + if ('callStructuredModel' in this.model) { + return this.model.callStructuredModel(prompt); + } else { + const modelWithStructuredOutput = this.model.withStructuredOutput(schema, { + method: 'jsonSchema', + strict: true, + includeRaw: true, + }); + const res = await modelWithStructuredOutput.invoke(prompt); - logger.debug('LLm response', res); + logger.debug('LLm response', res); - if (res.parsed) { - return res.parsed; - } + if (res.parsed) { + return res.parsed; + } - return parseJson(res.raw.content[0] as string); + return parseJson(res.raw.content[0] as string); + } } } @@ -80,3 +98,37 @@ const parseJson = (json: string) => { logger.debug('Escaped JSON', jsonString); return JSON.parse(jsonString); }; + +interface DeepSeekOptions { + apiKey: string; + baseURL: string; + temperature: number; // https://api-docs.deepseek.com/quick_start/parameter_settings 0.0 | 1.0 | 1.3 | 1.5 + model: Extract; +} + +function createDeepSeekModel(options: DeepSeekOptions) { + const { apiKey, baseURL, temperature = 1.0, model } = options; + + const client = new DeepSeekAI({ baseURL, apiKey }); + + return { + callModel: async (prompt: string) => { + const completion = await client.chat.completions.create({ + messages: [{ role: 'user', content: prompt }], + model, + temperature, + }); + return completion.choices[0].message.content as string; + }, + + callStructuredModel: async (prompt: string): Promise => { + const completion = await client.chat.completions.create({ + messages: [{ role: 'user', content: prompt }], + model, + temperature, + }); + const content = completion.choices[0].message.content; + return parseJson(content as string); + }, + }; +} diff --git a/src/common/types.ts b/src/common/types.ts index 6e7fb8a3..41a42cbe 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,5 +1,6 @@ import type { z } from 'zod'; import type { feedbackSchema, reviewSchema } from '../review/prompt/schemas'; +import type { AIModelName } from '../review/constants'; export type AskAIResponse = { markdownReport: string; @@ -35,17 +36,23 @@ export enum PlatformOptions { AZDEV = 'azdev', } +export type CIOptions = 'github' | 'gitlab' | 'azdev'; + +export type ProviderOptions = 'openai' | 'azureai' | 'bedrock' | 'deepseek'; + +export type ReviewType = 'full' | 'changed' | 'costOptimized'; + export type ReviewArgs = { [x: string]: unknown; - ci: string | undefined; - setupTarget: string; + ci: CIOptions | undefined; + setupTarget: CIOptions; commentPerFile: boolean; - model: string; - reviewType: string; + model: AIModelName; + reviewType: ReviewType; reviewLanguage: string | undefined; org: string | undefined; remote: string | undefined; - provider: string; + provider: ProviderOptions; _: (string | number)[]; $0: string; }; diff --git a/src/review/constants.ts b/src/review/constants.ts index acfb1322..32992936 100644 --- a/src/review/constants.ts +++ b/src/review/constants.ts @@ -1,6 +1,8 @@ export const signOff = '#### Powered by [Code Review GPT](https://github.com/mattzcarey/code-review-gpt)'; +export type AIModelName = (typeof modelInfo)[number]['model']; + export const modelInfo = [ { model: 'gpt-4o-mini', @@ -34,9 +36,19 @@ export const modelInfo = [ model: 'gpt-3.5-turbo-16k', maxPromptLength: 45000, //16k tokens }, -]; // Response needs about 1k tokens ~= 3k characters + { + model: 'deepseek-chat', + maxPromptLength: 180000, //64k tokens + }, + { + model: 'deepseek-reasoner', + maxPromptLength: 180000, //64k tokens + }, +] as const; // Response needs about 1k tokens ~= 3k characters + +export const modelsNames = modelInfo.map((item) => item.model); -export const languageMap: { [key: string]: string } = { +export const languageMap = { '.js': 'JavaScript', '.ts': 'TypeScript', '.py': 'Python', @@ -61,7 +73,7 @@ export const languageMap: { [key: string]: string } = { '.tf': 'Terraform', '.hcl': 'Terraform', '.swift': 'Swift', -}; +} as const; export const supportedFiles = new Set(Object.keys(languageMap)); diff --git a/src/review/llm/askAI.ts b/src/review/llm/askAI.ts index e395d006..b3f8a063 100644 --- a/src/review/llm/askAI.ts +++ b/src/review/llm/askAI.ts @@ -3,13 +3,16 @@ import type { AskAIResponse } from '../../common/types'; import { logger } from '../../common/utils/logger'; import { processFeedbacks } from './feedbackProcessor'; import { markdownReport } from './generateMarkdownReport'; +import { AIModelName } from '../constants'; + +import type { ProviderOptions } from '../../common/types'; export const askAI = async ( prompts: string[], - modelName: string, + modelName: AIModelName, openAIApiKey: string, organization: string | undefined, - provider: string + provider: ProviderOptions ): Promise => { logger.info('Asking the experts...'); diff --git a/src/test/run/runTest.ts b/src/test/run/runTest.ts index 3b70aeda..db9e77c0 100644 --- a/src/test/run/runTest.ts +++ b/src/test/run/runTest.ts @@ -5,6 +5,8 @@ import { logger } from '../../common/utils/logger'; import { askAI } from '../../review/llm/askAI'; import { constructPromptsArray } from '../../review/prompt/constructPrompt/constructPrompt'; import type { TestCase } from '../types'; +import type { AIModelName } from '../../review/constants'; +import type { ReviewType } from '../../common/types'; import { generateTestReport, generateTestResultsSummary, @@ -25,10 +27,10 @@ import { const runTest = async ( openAIApiKey: string, testCase: TestCase, - modelName: string, + modelName: AIModelName, maxPromptLength: number, vectorStore: MemoryVectorStore, - reviewType: string, + reviewType: ReviewType, reviewLanguage?: string // eslint-disable-next-line max-params ): Promise => { @@ -88,10 +90,10 @@ const runTest = async ( export const runTests = async ( openAIApiKey: string, testCases: TestCase[], - modelName: string, + modelName: AIModelName, maxPromptLength: number, vectorStore: MemoryVectorStore, - reviewType: string, + reviewType: ReviewType, reviewLanguage?: string // eslint-disable-next-line max-params ): Promise => {