@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 CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  # AI Code Reviewer
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/@krotovm/gitlab-ai-review)](https://www.npmjs.com/package/@krotovm/gitlab-ai-review)
6
+ [![downloads](https://img.shields.io/npm/dm/@krotovm/gitlab-ai-review)](https://www.npmjs.com/package/@krotovm/gitlab-ai-review)
7
+ [![license](https://img.shields.io/npm/l/@krotovm/gitlab-ai-review)](https://github.com/KrotovM/gitlab-ai-mr-reviewer/blob/main/LICENSE)
8
+ [![GitLab CI/CD Catalog](https://img.shields.io/gitlab/v/release/KrotovM/gitlab-ai-review?label=GitLab%20CI%2FCD%20Catalog&color=fc6d26)](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
+ ![AI review comment posted on a GitLab merge request](docs/review.png)
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
+ ![AI review debug report artifact](docs/report.png)
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=5` - Parallel per-file review API calls (CI multi-pass only).
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
+ ![Multi-pass pipeline in the CI job log](docs/job.png)
@@ -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 reviewConcurrency = parseNumberFlag(process.argv, "max-review-concurrency", DEFAULT_REVIEW_CONCURRENCY, 1);
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;
@@ -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 keeps self-hosted gateways under their proxy timeout; raise via
57
- // --max-review-concurrency on backends that batch well.
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 [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@krotovm/gitlab-ai-review",
4
- "version": "1.0.39",
4
+ "version": "1.0.41",
5
5
  "description": "CLI tool to generate AI code reviews for GitLab merge requests.",
6
6
  "main": "dist/cli.js",
7
7
  "bin": {