@krotovm/gitlab-ai-review 1.0.44 → 1.0.45

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
@@ -7,7 +7,7 @@
7
7
  [![license](https://img.shields.io/npm/l/@krotovm/gitlab-ai-review)](https://github.com/KrotovM/gitlab-ai-mr-reviewer/blob/main/LICENSE)
8
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
9
 
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.
10
+ Gitlab AI Code Review is a CLI tool that leverages OpenAI or Anthropic (Claude) models to automatically review code changes and post a Markdown review to GitLab merge requests from CI.
11
11
 
12
12
  ![AI review comment posted on a GitLab merge request](docs/review.png)
13
13
 
@@ -72,9 +72,11 @@ The HTML artifact breaks the run down per pass — tokens, durations, triage dec
72
72
 
73
73
  Set these in your project/group CI settings:
74
74
 
75
- - `OPENAI_API_KEY` (required)
75
+ - `AI_PROVIDER` (optional, default: `openai`; one of `openai` \| `anthropic`)
76
+ - `OPENAI_API_KEY` (required for `openai`)
76
77
  - `OPENAI_BASE_URL` (optional, for OpenAI-compatible providers/proxies)
77
- - `AI_MODEL` (optional, default: `gpt-4o-mini`; example: `gpt-4o`)
78
+ - `ANTHROPIC_API_KEY` (required for `anthropic`)
79
+ - `AI_MODEL` (optional, default: `gpt-4o-mini` for `openai`, `claude-opus-5-5` for `anthropic`; examples: `gpt-4o`, `claude-sonnet-5-5`)
78
80
  - `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.
79
81
  - `PROJECT_ACCESS_TOKEN` (required; project or personal access token with `api` scope — `CI_JOB_TOKEN` can't post MR comments or use the search API)
80
82
  - `GITLAB_TOKEN` (optional alias for `PROJECT_ACCESS_TOKEN`)
@@ -83,6 +85,8 @@ Set these in your project/group CI settings:
83
85
 
84
86
  `OPENAI_BASE_URL` is passed through to the `openai` SDK client, so you can use any OpenAI-compatible gateway/provider endpoint.
85
87
 
88
+ With `AI_PROVIDER=anthropic` the review calls the Anthropic Messages API directly (`ANTHROPIC_BASE_URL` overrides the endpoint the same way). Tool rounds use prompt caching, and a request declined by Claude's safety classifiers is re-run server-side on Anthropic's recommended fallback model. `--force-tools` is ignored there (Claude Opus/Sonnet 5.5 reject forced tool calls).
89
+
86
90
  GitLab provides these automatically in Merge Request pipelines:
87
91
 
88
92
  - `CI_API_V4_URL`
@@ -0,0 +1,138 @@
1
+ /** @format */
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ export const DEFAULT_ANTHROPIC_MODEL = "claude-opus-5-5";
4
+ /** Raw assistant turns by their first tool_use id. The review loops rebuild
5
+ * assistant messages from content + tool_calls, but Claude needs its thinking
6
+ * blocks back verbatim, and the closing no-tools round must resend the same
7
+ * tools: both are part of the prefix the API validates thinking blocks against. */
8
+ const turns = new Map();
9
+ const textOf = (content) => typeof content === "string"
10
+ ? content
11
+ : Array.isArray(content)
12
+ ? content.map((part) => part?.text ?? "").join("")
13
+ : "";
14
+ /** Runs an OpenAI-shaped chat request (as built in ci-review.ts) on the Anthropic
15
+ * Messages API and returns the answer as a ChatCompletion. `temperature` and
16
+ * `response_format` are dropped: Claude 4.7+ reject non-default sampling, and the
17
+ * triage parser already tolerates replies without JSON mode. */
18
+ export async function createAnthropicCompletion(client, request, signal) {
19
+ let tools = request.tools?.flatMap((tool) => tool.type === "function"
20
+ ? [
21
+ {
22
+ name: tool.function.name,
23
+ description: tool.function.description,
24
+ input_schema: tool.function.parameters,
25
+ },
26
+ ]
27
+ : []);
28
+ const system = [];
29
+ const messages = [];
30
+ // Tool results of one round, and any user text after them, share one user turn.
31
+ const pushUser = (block) => {
32
+ const last = messages.at(-1);
33
+ if (last?.role === "user" && Array.isArray(last.content)) {
34
+ last.content.push(block);
35
+ }
36
+ else {
37
+ messages.push({ role: "user", content: [block] });
38
+ }
39
+ };
40
+ for (const message of request.messages) {
41
+ if (message.role === "system" || message.role === "developer") {
42
+ system.push(textOf(message.content));
43
+ }
44
+ else if (message.role === "user") {
45
+ pushUser({ type: "text", text: textOf(message.content) });
46
+ }
47
+ else if (message.role === "tool") {
48
+ pushUser({
49
+ type: "tool_result",
50
+ tool_use_id: message.tool_call_id,
51
+ content: textOf(message.content),
52
+ });
53
+ }
54
+ else if (message.role === "assistant") {
55
+ const turn = turns.get(message.tool_calls?.[0]?.id ?? "");
56
+ tools ??= turn?.tools;
57
+ messages.push({
58
+ role: "assistant",
59
+ content: turn?.content ?? textOf(message.content),
60
+ });
61
+ }
62
+ }
63
+ const response = await client.beta.messages
64
+ .stream({
65
+ model: request.model,
66
+ max_tokens: 64_000,
67
+ ...(system.length > 0 && { system: system.join("\n\n") }),
68
+ messages,
69
+ // Tool rounds resend the growing history, so cache it. --force-tools maps
70
+ // to auto: Claude Opus/Sonnet 5.5 reject forced tool use. The closing round
71
+ // after the tool limit turns the tools off instead of dropping them.
72
+ ...(tools && {
73
+ tools,
74
+ tool_choice: request.tools ? { type: "auto" } : { type: "none" },
75
+ cache_control: { type: "ephemeral" },
76
+ }),
77
+ // A safety-classifier refusal is re-run on Anthropic's recommended fallback model.
78
+ betas: ["server-side-fallback-2026-07-01"],
79
+ fallbacks: "default",
80
+ }, { signal })
81
+ .finalMessage();
82
+ if (response.stop_reason === "refusal") {
83
+ throw new Error(`Model refused the request (category: ${response.stop_details?.category ?? "none"}).`);
84
+ }
85
+ // After a mid-output fallback only the declined model's text carries over; its
86
+ // thinking and tool calls are not part of the conversation the fallback continued.
87
+ const cut = response.content.map((block) => block.type).lastIndexOf("fallback");
88
+ const content = response.content.filter((block, i) => i >= cut || block.type === "text");
89
+ const toolUses = content.filter((block) => block.type === "tool_use");
90
+ if (toolUses[0] != null) {
91
+ turns.set(toolUses[0].id, { content, tools: tools ?? [] });
92
+ }
93
+ const usage = response.usage;
94
+ const promptTokens = usage.input_tokens +
95
+ (usage.cache_creation_input_tokens ?? 0) +
96
+ (usage.cache_read_input_tokens ?? 0);
97
+ return {
98
+ id: response.id,
99
+ object: "chat.completion",
100
+ created: Math.floor(Date.now() / 1000),
101
+ model: response.model,
102
+ // Raw fields stay too, so debug artifacts show cache reads.
103
+ usage: {
104
+ ...usage,
105
+ prompt_tokens: promptTokens,
106
+ completion_tokens: usage.output_tokens,
107
+ total_tokens: promptTokens + usage.output_tokens,
108
+ },
109
+ choices: [
110
+ {
111
+ index: 0,
112
+ logprobs: null,
113
+ finish_reason: toolUses.length > 0
114
+ ? "tool_calls"
115
+ : response.stop_reason === "max_tokens"
116
+ ? "length"
117
+ : "stop",
118
+ message: {
119
+ role: "assistant",
120
+ content: content
121
+ .flatMap((block) => (block.type === "text" ? [block.text] : []))
122
+ .join(""),
123
+ refusal: null,
124
+ ...(toolUses.length > 0 && {
125
+ tool_calls: toolUses.map((block) => ({
126
+ id: block.id,
127
+ type: "function",
128
+ function: {
129
+ name: block.name,
130
+ arguments: JSON.stringify(block.input),
131
+ },
132
+ })),
133
+ }),
134
+ },
135
+ },
136
+ ],
137
+ };
138
+ }
@@ -1,5 +1,7 @@
1
1
  /** @format */
2
+ import Anthropic from "@anthropic-ai/sdk";
2
3
  import OpenAI from "openai";
4
+ import { createAnthropicCompletion } from "./anthropic.js";
3
5
  import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, DEFAULT_PROMPT_PROFILE, DISCLAIMER, extractCompletionText, isEmptyReviewBody, NO_FINDINGS_SENTENCE, parseTriageResponseDetailed, parseTriageResponse, } from "../prompt/index.js";
4
6
  import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
5
7
  import { parseReviewFindings, truncateWithMarker } from "../prompt/utils.js";
@@ -67,7 +69,7 @@ async function appendDebugDump(_debugDumpFile, debugRecordWriter, record) {
67
69
  }
68
70
  }
69
71
  async function createCompletionWithDebug(params) {
70
- const { openaiInstance, requestLabel, request, debugDumpFile, debugRecordWriter } = params;
72
+ const { llm, requestLabel, request, debugDumpFile, debugRecordWriter } = params;
71
73
  await appendDebugDump(debugDumpFile, debugRecordWriter, {
72
74
  kind: "openai_request",
73
75
  label: requestLabel,
@@ -78,11 +80,13 @@ async function createCompletionWithDebug(params) {
78
80
  // The SDK's own timeout only covers time-to-headers; a stalled SSE body would hang
79
81
  // forever without the explicit AbortSignal deadline.
80
82
  const { stream: _stream, ...createParams } = request;
81
- const attempt = (withUsage) => openaiInstance.chat.completions
82
- .stream(withUsage
83
- ? { ...createParams, stream_options: { include_usage: true } }
84
- : createParams, { signal: AbortSignal.timeout(COMPLETION_TIMEOUT_MS) })
85
- .finalChatCompletion();
83
+ const attempt = (withUsage) => llm instanceof Anthropic
84
+ ? createAnthropicCompletion(llm, createParams, AbortSignal.timeout(COMPLETION_TIMEOUT_MS))
85
+ : llm.chat.completions
86
+ .stream(withUsage
87
+ ? { ...createParams, stream_options: { include_usage: true } }
88
+ : createParams, { signal: AbortSignal.timeout(COMPLETION_TIMEOUT_MS) })
89
+ .finalChatCompletion();
86
90
  try {
87
91
  let completion;
88
92
  try {
@@ -261,7 +265,7 @@ async function mapWithConcurrency(items, concurrency, fn) {
261
265
  return results;
262
266
  }
263
267
  async function reviewMergeRequestWithTools(params) {
264
- const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, headers, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, loggers, debugDumpFile, debugRecordWriter, } = params;
268
+ const { llm, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, headers, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, loggers, debugDumpFile, debugRecordWriter, } = params;
265
269
  const { logDebug, logStep } = loggers;
266
270
  logStep(`Single-pass review started for ${changes.length} file(s) (fallback mode).`);
267
271
  const messages = buildPrompt({
@@ -320,7 +324,7 @@ async function reviewMergeRequestWithTools(params) {
320
324
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
321
325
  logStep(`Single-pass round ${round + 1}/${MAX_TOOL_ROUNDS}: requesting model response...`);
322
326
  const completion = await createCompletionWithDebug({
323
- openaiInstance,
327
+ llm,
324
328
  requestLabel: `main_review_round_${round + 1}`,
325
329
  debugDumpFile,
326
330
  debugRecordWriter,
@@ -395,7 +399,7 @@ async function reviewMergeRequestWithTools(params) {
395
399
  });
396
400
  logStep("Single-pass tool-call limit reached. Requesting final answer.");
397
401
  const finalCompletion = await createCompletionWithDebug({
398
- openaiInstance,
402
+ llm,
399
403
  requestLabel: "main_review_final_after_tool_limit",
400
404
  debugDumpFile,
401
405
  debugRecordWriter,
@@ -409,7 +413,7 @@ async function reviewMergeRequestWithTools(params) {
409
413
  return buildAnswer(finalCompletion);
410
414
  }
411
415
  async function runFileReviewWithTools(params) {
412
- const { openaiInstance, aiModel, filePath, fileDiff, summary, otherChangedFiles, refs, gitLabProjectApiUrl, headers, forceTools, promptProfile, loggers, debugDumpFile, debugRecordWriter, } = params;
416
+ const { llm, aiModel, filePath, fileDiff, summary, otherChangedFiles, refs, gitLabProjectApiUrl, headers, forceTools, promptProfile, loggers, debugDumpFile, debugRecordWriter, } = params;
413
417
  const { logDebug, logStep } = loggers;
414
418
  const messages = buildFileReviewPrompt({
415
419
  filePath,
@@ -464,7 +468,7 @@ async function runFileReviewWithTools(params) {
464
468
  ];
465
469
  for (let round = 0; round < MAX_FILE_TOOL_ROUNDS; round += 1) {
466
470
  const completion = await createCompletionWithDebug({
467
- openaiInstance,
471
+ llm,
468
472
  requestLabel: `file_review_${filePath}_round_${round + 1}`,
469
473
  debugDumpFile,
470
474
  debugRecordWriter,
@@ -539,7 +543,7 @@ async function runFileReviewWithTools(params) {
539
543
  content: "Tool-call limit reached. Provide your final review now without any tool calls.",
540
544
  });
541
545
  const final = await createCompletionWithDebug({
542
- openaiInstance,
546
+ llm,
543
547
  requestLabel: `file_review_${filePath}_final_after_tool_limit`,
544
548
  debugDumpFile,
545
549
  debugRecordWriter,
@@ -556,7 +560,7 @@ function draftHasStructuredFindings(consolidatedText) {
556
560
  return /-\s*\[(?:high|medium)\]/i.test(consolidatedText);
557
561
  }
558
562
  async function runVerificationWithTools(params) {
559
- const { openaiInstance, aiModel, baseMessages, refs, gitLabProjectApiUrl, headers, forceTools, consolidatedDraft, loggers, debugDumpFile, debugRecordWriter, } = params;
563
+ const { llm, aiModel, baseMessages, refs, gitLabProjectApiUrl, headers, forceTools, consolidatedDraft, loggers, debugDumpFile, debugRecordWriter, } = params;
560
564
  const { logDebug, logStep } = loggers;
561
565
  const messages = [...baseMessages];
562
566
  const tools = [
@@ -605,7 +609,7 @@ async function runVerificationWithTools(params) {
605
609
  const verificationForceRound0 = forceTools && draftHasStructuredFindings(consolidatedDraft);
606
610
  for (let round = 0; round < MAX_VERIFICATION_TOOL_ROUNDS; round += 1) {
607
611
  const completion = await createCompletionWithDebug({
608
- openaiInstance,
612
+ llm,
609
613
  requestLabel: `verification_pass_round_${round + 1}`,
610
614
  debugDumpFile,
611
615
  debugRecordWriter,
@@ -678,7 +682,7 @@ async function runVerificationWithTools(params) {
678
682
  content: `Tool-call limit reached (${MAX_VERIFICATION_TOOL_ROUNDS}). Do not call tools. Output only the verified findings in the required format.`,
679
683
  });
680
684
  return createCompletionWithDebug({
681
- openaiInstance,
685
+ llm,
682
686
  requestLabel: "verification_pass_final_after_tool_limit",
683
687
  debugDumpFile,
684
688
  debugRecordWriter,
@@ -691,7 +695,7 @@ async function runVerificationWithTools(params) {
691
695
  });
692
696
  }
693
697
  export async function reviewMergeRequestMultiPass(params) {
694
- const { openaiInstance, aiModel, promptLimits, triageDiffChars, changes, refs, gitLabProjectApiUrl, headers, maxFindings, reviewConcurrency, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, allowProfileAutoDemote = false, loggers, debugDumpFile, debugRecordWriter, } = params;
698
+ const { llm, aiModel, promptLimits, triageDiffChars, changes, refs, gitLabProjectApiUrl, headers, maxFindings, reviewConcurrency, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, allowProfileAutoDemote = false, loggers, debugDumpFile, debugRecordWriter, } = params;
695
699
  const { logStep } = loggers;
696
700
  let effectiveProfile = promptProfile;
697
701
  logStep(`Pass 1/4: triaging ${changes.length} file(s) ` +
@@ -710,7 +714,7 @@ export async function reviewMergeRequestMultiPass(params) {
710
714
  let triageParseError = null;
711
715
  try {
712
716
  const triageCompletion = await createCompletionWithDebug({
713
- openaiInstance,
717
+ llm,
714
718
  requestLabel: "triage_pass",
715
719
  debugDumpFile,
716
720
  debugRecordWriter,
@@ -745,7 +749,7 @@ export async function reviewMergeRequestMultiPass(params) {
745
749
  logStep("Triage response unparseable with prompt profile=default. Retrying triage with profile=weak.");
746
750
  try {
747
751
  const weakTriageCompletion = await createCompletionWithDebug({
748
- openaiInstance,
752
+ llm,
749
753
  requestLabel: "triage_pass_weak_retry",
750
754
  debugDumpFile,
751
755
  debugRecordWriter,
@@ -786,7 +790,7 @@ export async function reviewMergeRequestMultiPass(params) {
786
790
  }
787
791
  logStep("Falling back to single-pass pipeline.");
788
792
  return await reviewMergeRequestWithTools({
789
- openaiInstance,
793
+ llm,
790
794
  aiModel,
791
795
  promptLimits,
792
796
  changes,
@@ -831,7 +835,7 @@ export async function reviewMergeRequestMultiPass(params) {
831
835
  let findings;
832
836
  try {
833
837
  findings = await runFileReviewWithTools({
834
- openaiInstance,
838
+ llm,
835
839
  aiModel,
836
840
  filePath: change.new_path,
837
841
  fileDiff: truncateWithMarker(change.diff, promptLimits.maxDiffChars, change.new_path),
@@ -894,7 +898,7 @@ export async function reviewMergeRequestMultiPass(params) {
894
898
  }
895
899
  try {
896
900
  const consolidateCompletion = await createCompletionWithDebug({
897
- openaiInstance,
901
+ llm,
898
902
  requestLabel: "consolidate_pass",
899
903
  debugDumpFile,
900
904
  debugRecordWriter,
@@ -920,7 +924,7 @@ export async function reviewMergeRequestMultiPass(params) {
920
924
  });
921
925
  try {
922
926
  const verificationCompletion = await runVerificationWithTools({
923
- openaiInstance,
927
+ llm,
924
928
  aiModel,
925
929
  baseMessages: verificationMessages,
926
930
  refs,
package/dist/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /** @format */
3
+ import Anthropic from "@anthropic-ai/sdk";
3
4
  import OpenAI from "openai";
4
5
  import { readFile } from "node:fs/promises";
5
6
  import { DEFAULT_MAX_FINDINGS, DEFAULT_REVIEW_CONCURRENCY, } from "./prompt/index.js";
6
7
  import { envOrDefault, envOrUndefined, hasDebugFlag, hasForceToolsFlag, hasIncludeArtifactsFlag, hasIgnoredExtension, hasInlineCommentsFlag, parseIgnoreExtensions, parseNumberFlag, parsePromptLimits, parseTriageDiffChars, readPromptProfileFromEnv, requireEnvs, } from "./cli/args.js";
8
+ import { DEFAULT_ANTHROPIC_MODEL } from "./cli/anthropic.js";
7
9
  import { reviewMergeRequestMultiPass } from "./cli/ci-review.js";
8
10
  import { postInlineFindings } from "./cli/inline-comments.js";
9
11
  import { fetchMergeRequestChanges, postMergeRequestNote, } from "./gitlab/services.js";
@@ -20,7 +22,7 @@ Usage:
20
22
  Debug:
21
23
  --debug Print full error details (stack, API error fields).
22
24
  --include-artifacts Generate local HTML artifact without printing payloads to console.
23
- --force-tools Force at least one tool-call round in tool-enabled review paths.
25
+ --force-tools Force at least one tool-call round in tool-enabled review paths (openai only).
24
26
  --ignore-ext Ignore file extensions (comma-separated only). Example: --ignore-ext=md,lock
25
27
  --max-diffs=50
26
28
  --max-diff-chars=16000
@@ -31,9 +33,11 @@ Debug:
31
33
  --inline-comments Post findings on changed lines as resolvable diff threads; the rest stays in the summary comment.
32
34
 
33
35
  Env vars:
34
- OPENAI_API_KEY (required) OpenAI API key.
36
+ AI_PROVIDER (optional) "openai" | "anthropic". Default: openai.
37
+ OPENAI_API_KEY (required for openai) OpenAI API key.
35
38
  OPENAI_BASE_URL (optional) Custom OpenAI-compatible API base URL.
36
- AI_MODEL (optional) OpenAI chat model, e.g. gpt-4o. Default: gpt-4o-mini.
39
+ ANTHROPIC_API_KEY (required for anthropic) Anthropic API key.
40
+ AI_MODEL (optional) Chat model. Default: gpt-4o-mini (openai), ${DEFAULT_ANTHROPIC_MODEL} (anthropic).
37
41
  AI_REVIEW_CONCURRENCY (optional) Same as --max-review-concurrency; the flag wins.
38
42
  AI_PROMPT_PROFILE (optional) Prompt style: "default" | "weak". Default: default.
39
43
  Use "weak" for small/quantized models — shorter prompts,
@@ -88,7 +92,11 @@ async function main() {
88
92
  const reviewConcurrency = parseNumberFlag(process.argv, "max-review-concurrency", Number.isFinite(envConcurrency) && envConcurrency >= 1
89
93
  ? Math.floor(envConcurrency)
90
94
  : DEFAULT_REVIEW_CONCURRENCY, 1);
91
- const aiModel = envOrDefault("AI_MODEL", "gpt-4o-mini");
95
+ const provider = envOrDefault("AI_PROVIDER", "openai").toLowerCase();
96
+ if (provider !== "openai" && provider !== "anthropic") {
97
+ throw new Error(`AI_PROVIDER must be "openai" or "anthropic", got "${provider}".`);
98
+ }
99
+ const aiModel = envOrDefault("AI_MODEL", provider === "anthropic" ? DEFAULT_ANTHROPIC_MODEL : "gpt-4o-mini");
92
100
  const promptProfile = readPromptProfileFromEnv();
93
101
  const allowProfileAutoDemote = envOrUndefined("AI_PROMPT_PROFILE") == null;
94
102
  logStep(`Prompt profile: ${promptProfile}${allowProfileAutoDemote ? " (auto: falls back to weak if triage JSON fails)" : ""}`);
@@ -104,8 +112,9 @@ async function main() {
104
112
  let artifactWritten = false;
105
113
  const loggers = { logStep, logDebug };
106
114
  const projectAccessToken = envOrUndefined("PROJECT_ACCESS_TOKEN") ?? envOrUndefined("GITLAB_TOKEN");
115
+ const apiKeyEnv = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
107
116
  const gitlabRequired = [
108
- "OPENAI_API_KEY",
117
+ apiKeyEnv,
109
118
  "CI_API_V4_URL",
110
119
  "CI_PROJECT_ID",
111
120
  "CI_MERGE_REQUEST_IID",
@@ -114,7 +123,7 @@ async function main() {
114
123
  if (projectAccessToken == null)
115
124
  gitlabRequired.push("PROJECT_ACCESS_TOKEN");
116
125
  const envs = requireEnvs(gitlabRequired);
117
- const openaiApiKey = envs["OPENAI_API_KEY"];
126
+ const apiKey = envs[apiKeyEnv];
118
127
  const ciApiV4Url = envs["CI_API_V4_URL"];
119
128
  // MR endpoints live in the MR's target project; in fork pipelines CI_PROJECT_ID is the fork.
120
129
  const projectId = envOrUndefined("CI_MERGE_REQUEST_PROJECT_ID") ?? envs["CI_PROJECT_ID"];
@@ -139,9 +148,11 @@ async function main() {
139
148
  process.stdout.write("No changes found in merge request. Skipping review.\n");
140
149
  return;
141
150
  }
142
- logStep(`Requesting AI review with model: ${aiModel} (multi-pass pipeline)`);
151
+ logStep(`Requesting AI review with model: ${aiModel} (${provider}, multi-pass pipeline)`);
143
152
  const answer = await reviewMergeRequestMultiPass({
144
- openaiInstance: new OpenAI({ apiKey: openaiApiKey }),
153
+ llm: provider === "anthropic"
154
+ ? new Anthropic({ apiKey })
155
+ : new OpenAI({ apiKey }),
145
156
  aiModel,
146
157
  promptLimits,
147
158
  triageDiffChars,
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.44",
4
+ "version": "1.0.45",
5
5
  "description": "CLI tool to generate AI code reviews for GitLab merge requests.",
6
6
  "main": "dist/cli.js",
7
7
  "bin": {
@@ -24,6 +24,8 @@
24
24
  "merge-request",
25
25
  "code-review",
26
26
  "openai",
27
+ "anthropic",
28
+ "claude",
27
29
  "cli"
28
30
  ],
29
31
  "author": "KrotovM",
@@ -40,6 +42,7 @@
40
42
  },
41
43
  "homepage": "https://github.com/KrotovM/gitlab-ai-mr-reviewer#readme",
42
44
  "dependencies": {
45
+ "@anthropic-ai/sdk": "^0.133.0",
43
46
  "openai": "^6.33.0"
44
47
  },
45
48
  "devDependencies": {