@krotovm/gitlab-ai-review 1.0.39 → 1.0.41
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/README.md +44 -1
- package/dist/cli/ci-review.js +20 -4
- package/dist/cli.js +5 -1
- package/dist/prompt/index.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
# AI Code Reviewer
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@krotovm/gitlab-ai-review)
|
|
6
|
+
[](https://www.npmjs.com/package/@krotovm/gitlab-ai-review)
|
|
7
|
+
[](https://github.com/KrotovM/gitlab-ai-mr-reviewer/blob/main/LICENSE)
|
|
8
|
+
[](https://gitlab.com/explore/catalog/KrotovM/gitlab-ai-review)
|
|
9
|
+
|
|
5
10
|
Gitlab AI Code Review is a CLI tool that leverages OpenAI models to automatically review code changes and post a Markdown review to GitLab merge requests from CI.
|
|
6
11
|
|
|
12
|
+

|
|
13
|
+
|
|
7
14
|
## Features
|
|
8
15
|
|
|
9
16
|
- Automatically reviews code changes in GitLab repositories
|
|
@@ -30,6 +37,15 @@ ai_review:
|
|
|
30
37
|
- npx -y @krotovm/gitlab-ai-review
|
|
31
38
|
```
|
|
32
39
|
|
|
40
|
+
Or include it as a [CI/CD component from the GitLab Catalog](https://gitlab.com/explore/catalog/KrotovM/gitlab-ai-review) (gitlab.com):
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
include:
|
|
44
|
+
- component: gitlab.com/KrotovM/gitlab-ai-review/review@1.0.2
|
|
45
|
+
inputs:
|
|
46
|
+
args: "--include-artifacts"
|
|
47
|
+
```
|
|
48
|
+
|
|
33
49
|
Save debug HTML as a CI artifact:
|
|
34
50
|
|
|
35
51
|
```yaml
|
|
@@ -48,6 +64,10 @@ ai_review:
|
|
|
48
64
|
- ai-review-report.html
|
|
49
65
|
```
|
|
50
66
|
|
|
67
|
+
The HTML artifact breaks the run down per pass — tokens, durations, triage decisions, per-file findings, and errors:
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
51
71
|
## Env variables
|
|
52
72
|
|
|
53
73
|
Set these in your project/group CI settings:
|
|
@@ -58,6 +78,7 @@ Set these in your project/group CI settings:
|
|
|
58
78
|
- `AI_PROMPT_PROFILE` (optional, default: `default`; one of `default` \| `weak`). Use `weak` for small or heavily-quantized models (≤ ~7B, local Ollama, etc.). The `weak` profile uses shorter system prompts, positive rules instead of negations, and inline few-shot examples for triage / per-file review / consolidation / verification, which dramatically improves output-format adherence on weak models. When the variable is not set, the profile is auto-detected: if the triage pass returns unparseable JSON with `default` prompts, triage is retried once with `weak` and, on success, the whole run continues with it. Set the variable explicitly to pin a profile and disable auto-detection.
|
|
59
79
|
- `PROJECT_ACCESS_TOKEN` (optional for public projects, but required for most private projects; token with `api` scope)
|
|
60
80
|
- `GITLAB_TOKEN` (optional alias for `PROJECT_ACCESS_TOKEN`)
|
|
81
|
+
- `AI_REVIEW_CONCURRENCY` (optional) — same as `--max-review-concurrency`, handy as a CI/CD variable; the flag wins when both are set.
|
|
61
82
|
- `AI_REVIEW_ARTIFACT_HTML_FILE` (optional, default: `ai-review-report.html`; used with `--include-artifacts`)
|
|
62
83
|
|
|
63
84
|
`OPENAI_BASE_URL` is passed through to the `openai` SDK client, so you can use any OpenAI-compatible gateway/provider endpoint.
|
|
@@ -77,11 +98,29 @@ GitLab provides these automatically in Merge Request pipelines:
|
|
|
77
98
|
- `--max-total-prompt-chars=220000` - Final hard cap for prompt size (single-pass fallback only).
|
|
78
99
|
- `--triage-diff-chars=2000` - Max chars per file diff sent to the triage pass (Pass 1). Increase for large diffs where the first hunks are mostly git headers.
|
|
79
100
|
- `--max-findings=5` - Max findings in the final review (CI multi-pass only).
|
|
80
|
-
- `--max-review-concurrency=
|
|
101
|
+
- `--max-review-concurrency=2` - Parallel per-file review API calls (CI multi-pass only). Set to 1 for single-GPU backends that queue-starve long requests.
|
|
81
102
|
- `--debug` - Print full error details (stack and API error fields).
|
|
82
103
|
- `--include-artifacts` - Generate a local HTML debug artifact with per-pass outputs/tokens.
|
|
83
104
|
- `--help` - Show help output.
|
|
84
105
|
|
|
106
|
+
## Benchmark
|
|
107
|
+
|
|
108
|
+
Head-to-head against [Alibaba OpenCodeReview](https://github.com/alibaba/open-code-review) v1.8.10 (via its official GitLab CI recipe) on a demo MR with 4 seeded bugs: an auth bypass (`API_KEYS` unset → empty key accepted), a rate limiter whose window never resets, an unbounded per-IP counters Map, and a null-deref crash in a stats endpoint. Same MR, same commit range, same backends.
|
|
109
|
+
|
|
110
|
+
| Tool | Model | Seeded bugs found | Noise (dups / off-target) | Tokens | LLM time |
|
|
111
|
+
| --- | --- | --- | --- | --- | --- |
|
|
112
|
+
| **gitlab-ai-review** | gpt-5.4 | 3 / 4 | 0 | 15.7k | ~17s |
|
|
113
|
+
| OpenCodeReview | gpt-5.4 | 3 / 4 | 2 | 41.5k | ~11s |
|
|
114
|
+
| **gitlab-ai-review** | self-hosted (single GPU) | **4 / 4** | **0** | 37.2k | 7m 24s |
|
|
115
|
+
| OpenCodeReview | self-hosted (single GPU) | 3 / 4 | 8 (incl. 5 duplicates) | 151.8k | 11m 45s |
|
|
116
|
+
|
|
117
|
+
Notes:
|
|
118
|
+
|
|
119
|
+
- On gpt-5.4 both tools found the same 3 bugs and both missed the memory leak. OpenCodeReview added two design opinions, one of which (severity "high") suggested removing the very auth feature the MR introduces.
|
|
120
|
+
- On the self-hosted model the multi-pass pipeline (triage → per-file review → consolidate → verify) paid off: all 4 seeded bugs, zero noise. OpenCodeReview posted 11 comments: 3 unique real findings, 5 duplicates, and 3 off-target — its only rate-limiter comment misdiagnosed the bug, and several comments were attributed to the wrong file or line.
|
|
121
|
+
- Weak-model reviews are output-bound on a single GPU: OpenCodeReview generated 40.2k output tokens vs our 18.5k, and consumed 6× our prompt tokens (111.6k vs 18.7k) — that difference is the wall-clock gap.
|
|
122
|
+
- Methodology caveats: single run per cell on one MR; LLM variance applies.
|
|
123
|
+
|
|
85
124
|
## Architecture
|
|
86
125
|
|
|
87
126
|
The reviewer uses a three-pass pipeline optimized for large merge requests:
|
|
@@ -91,3 +130,7 @@ The reviewer uses a three-pass pipeline optimized for large merge requests:
|
|
|
91
130
|
3. **Consolidate** - Per-file findings are merged, deduplicated, ranked by severity, and trimmed to top N (default 5).
|
|
92
131
|
|
|
93
132
|
If the triage pass fails (API error, unparseable response), the pipeline falls back to the original single-pass approach automatically.
|
|
133
|
+
|
|
134
|
+
The pipeline in a CI job log — triage, parallel tool-assisted file reviews, consolidation, verification:
|
|
135
|
+
|
|
136
|
+

|
package/dist/cli/ci-review.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, DEFAULT_PROMPT_PROFILE, extractCompletionText, isEmptyReviewBody, NO_FINDINGS_SENTENCE, parseTriageResponseDetailed, parseTriageResponse, } from "../prompt/index.js";
|
|
4
4
|
import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
|
|
5
|
-
import { truncateWithMarker } from "../prompt/utils.js";
|
|
5
|
+
import { parseReviewFindings, truncateWithMarker } from "../prompt/utils.js";
|
|
6
6
|
import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, MAX_VERIFICATION_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
7
7
|
/** Hard deadline per model call, retries included. Must exceed the gateway's own
|
|
8
8
|
* proxy timeout so the SDK's 5xx retries stay the first line of defense. */
|
|
@@ -849,6 +849,22 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
849
849
|
// every final-answer path gets it regardless of model formatting discipline.
|
|
850
850
|
const summaryText = triageResult.summary.trim().replace(/\s*\n\s*/g, " ");
|
|
851
851
|
const withHeader = (body) => `### 🤖 AI Code Review\n\n${summaryText === "" ? "" : `> ${summaryText}\n\n`}${body}`;
|
|
852
|
+
// Cap transparency: if consolidation likely trimmed the list (final count hit
|
|
853
|
+
// the cap while per-file review produced more candidates), say so in the note
|
|
854
|
+
// instead of silently dropping findings.
|
|
855
|
+
// ponytail: candidates deduped by file+title only; differently-worded dupes
|
|
856
|
+
// across files still inflate the count, which is why the note says "candidate".
|
|
857
|
+
const candidateCount = new Set(perFileFindings.flatMap((f) => parseReviewFindings(f.findings).map((x) => `${x.file}|${x.title.toLowerCase()}`))).size;
|
|
858
|
+
const withCapNote = (body) => {
|
|
859
|
+
const finalCount = parseReviewFindings(body).length;
|
|
860
|
+
if (finalCount < maxFindings || candidateCount <= finalCount)
|
|
861
|
+
return body;
|
|
862
|
+
const note = `\n\nℹ️ Showing top ${finalCount} of ${candidateCount} candidate findings (\`--max-findings=${maxFindings}\`). Run with \`--include-artifacts\` for the full list.`;
|
|
863
|
+
const disclaimerAt = body.lastIndexOf("\n\n---\n_");
|
|
864
|
+
return disclaimerAt === -1
|
|
865
|
+
? body + note
|
|
866
|
+
: body.slice(0, disclaimerAt) + note + body.slice(disclaimerAt);
|
|
867
|
+
};
|
|
852
868
|
logStep("Pass 3/4: consolidating findings");
|
|
853
869
|
const consolidateMessages = buildConsolidatePrompt({
|
|
854
870
|
perFileFindings,
|
|
@@ -875,7 +891,7 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
875
891
|
});
|
|
876
892
|
const consolidatedText = extractCompletionText(consolidateCompletion);
|
|
877
893
|
if (consolidatedText == null || consolidatedText.trim() === "") {
|
|
878
|
-
return withHeader(buildAnswer(consolidateCompletion));
|
|
894
|
+
return withHeader(withCapNote(buildAnswer(consolidateCompletion)));
|
|
879
895
|
}
|
|
880
896
|
logStep("Pass 4/4: verifying consolidated findings (repo tools)");
|
|
881
897
|
const verificationMessages = buildVerificationPrompt({
|
|
@@ -901,11 +917,11 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
901
917
|
debugDumpFile,
|
|
902
918
|
debugRecordWriter,
|
|
903
919
|
});
|
|
904
|
-
return withHeader(buildAnswer(verificationCompletion));
|
|
920
|
+
return withHeader(withCapNote(buildAnswer(verificationCompletion)));
|
|
905
921
|
}
|
|
906
922
|
catch (error) {
|
|
907
923
|
logStep(`Verification failed: ${error?.message ?? error}. Returning consolidated findings.`);
|
|
908
|
-
return withHeader(buildAnswer(consolidateCompletion));
|
|
924
|
+
return withHeader(withCapNote(buildAnswer(consolidateCompletion)));
|
|
909
925
|
}
|
|
910
926
|
}
|
|
911
927
|
catch (error) {
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ function printHelp() {
|
|
|
33
33
|
" OPENAI_API_KEY (required) OpenAI API key.",
|
|
34
34
|
" OPENAI_BASE_URL (optional) Custom OpenAI-compatible API base URL.",
|
|
35
35
|
" AI_MODEL (optional) OpenAI chat model, e.g. gpt-4o. Default: gpt-4o-mini.",
|
|
36
|
+
" AI_REVIEW_CONCURRENCY (optional) Same as --max-review-concurrency; the flag wins.",
|
|
36
37
|
" AI_PROMPT_PROFILE (optional) Prompt style: \"default\" | \"weak\". Default: default.",
|
|
37
38
|
" Use \"weak\" for small/quantized models — shorter prompts,",
|
|
38
39
|
" positive rules, and few-shot examples.",
|
|
@@ -82,7 +83,10 @@ async function main() {
|
|
|
82
83
|
const promptLimits = parsePromptLimits(process.argv);
|
|
83
84
|
const triageDiffChars = parseTriageDiffChars(process.argv);
|
|
84
85
|
const maxFindings = parseNumberFlag(process.argv, "max-findings", DEFAULT_MAX_FINDINGS, 1);
|
|
85
|
-
const
|
|
86
|
+
const envConcurrency = Number(envOrUndefined("AI_REVIEW_CONCURRENCY"));
|
|
87
|
+
const reviewConcurrency = parseNumberFlag(process.argv, "max-review-concurrency", Number.isFinite(envConcurrency) && envConcurrency >= 1
|
|
88
|
+
? Math.floor(envConcurrency)
|
|
89
|
+
: DEFAULT_REVIEW_CONCURRENCY, 1);
|
|
86
90
|
const aiModel = envOrDefault("AI_MODEL", "gpt-4o-mini");
|
|
87
91
|
const promptProfile = readPromptProfileFromEnv();
|
|
88
92
|
const allowProfileAutoDemote = envOrUndefined("AI_PROMPT_PROFILE") == null;
|
package/dist/prompt/index.js
CHANGED
|
@@ -53,8 +53,8 @@ export const buildPrompt = ({ changes, limits, allowTools = false, profile = DEF
|
|
|
53
53
|
// Multi-pass pipeline prompts
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
55
|
export const DEFAULT_MAX_FINDINGS = 5;
|
|
56
|
-
// ponytail: 2
|
|
57
|
-
//
|
|
56
|
+
// ponytail: 2 suits most gateways; single-GPU backends that queue-starve past
|
|
57
|
+
// the per-call deadline should set AI_REVIEW_CONCURRENCY=1 (or the flag).
|
|
58
58
|
export const DEFAULT_REVIEW_CONCURRENCY = 2;
|
|
59
59
|
export function buildTriagePrompt(changes, profile = DEFAULT_PROMPT_PROFILE, triageDiffChars = DEFAULT_TRIAGE_DIFF_CHARS) {
|
|
60
60
|
return [
|