@mrkaran/hodor 0.7.2 → 0.7.3

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.
@@ -31,6 +31,135 @@ function getTemplatePath(name) {
31
31
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", name);
32
32
  }
33
33
 
34
+ // src/review-diff.ts
35
+ var HODOR_REVIEW_SHA_RE = /^\s*<!--\s*hodor:sha:([a-f0-9]{40})\s*-->/i;
36
+ function getReviewDiffArgs(options) {
37
+ const {
38
+ platform,
39
+ targetBranch,
40
+ diffBaseSha,
41
+ previousReviewSha,
42
+ reviewDiffMode,
43
+ localMode = false
44
+ } = options;
45
+ const rebasedGitlabReview = platform === "gitlab" && reviewDiffMode === "snapshot";
46
+ if (previousReviewSha && !rebasedGitlabReview) {
47
+ return reviewDiffMode === "snapshot" ? ["--no-pager", "diff", previousReviewSha, "HEAD"] : ["--no-pager", "diff", `${previousReviewSha}...HEAD`];
48
+ }
49
+ if (localMode) return ["--no-pager", "diff", targetBranch];
50
+ if (diffBaseSha) return ["--no-pager", "diff", diffBaseSha, "HEAD"];
51
+ return ["--no-pager", "diff", `origin/${targetBranch}...HEAD`];
52
+ }
53
+ function getHodorReviewShaCandidates(notes) {
54
+ if (!notes || notes.length === 0) return [];
55
+ const candidates = [];
56
+ for (const [index, note] of notes.entries()) {
57
+ const match = note.body?.match(HODOR_REVIEW_SHA_RE);
58
+ if (!match) continue;
59
+ const createdAtMs = Date.parse(note.created_at ?? "");
60
+ candidates.push({
61
+ sha: match[1],
62
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
63
+ index
64
+ });
65
+ }
66
+ candidates.sort((a, b) => {
67
+ if (a.createdAtMs != null && b.createdAtMs != null && a.createdAtMs !== b.createdAtMs) {
68
+ return b.createdAtMs - a.createdAtMs;
69
+ }
70
+ if (a.createdAtMs != null && b.createdAtMs == null) return -1;
71
+ if (a.createdAtMs == null && b.createdAtMs != null) return 1;
72
+ return a.index - b.index;
73
+ });
74
+ return [...new Set(candidates.map(({ sha }) => sha))];
75
+ }
76
+ async function findLatestReviewBase(notes, workspacePath) {
77
+ const candidates = getHodorReviewShaCandidates(notes);
78
+ if (candidates.length === 0) return null;
79
+ logger.info(`Found ${candidates.length} previous Hodor review marker(s)`);
80
+ for (const sha of candidates) {
81
+ try {
82
+ let objectType;
83
+ try {
84
+ ({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
85
+ cwd: workspacePath
86
+ }));
87
+ } catch {
88
+ await exec("git", ["fetch", "--quiet", "origin", sha], {
89
+ cwd: workspacePath
90
+ });
91
+ ({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
92
+ cwd: workspacePath
93
+ }));
94
+ }
95
+ if (objectType.trim() !== "commit") throw new Error("not a commit");
96
+ try {
97
+ await exec("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
98
+ cwd: workspacePath
99
+ });
100
+ return { sha, mode: "incremental" };
101
+ } catch {
102
+ logger.info(
103
+ `Previous review SHA ${sha.slice(0, 8)} is not an ancestor; using snapshot delta`
104
+ );
105
+ return { sha, mode: "snapshot" };
106
+ }
107
+ } catch {
108
+ logger.info(
109
+ `Skipping previous review SHA ${sha.slice(0, 8)}; commit is unavailable`
110
+ );
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ function getDiffStats(diff) {
116
+ let files = 0;
117
+ let additions = 0;
118
+ let deletions = 0;
119
+ for (const line of diff.split("\n")) {
120
+ if (line.startsWith("diff --git ")) files++;
121
+ else if (line.startsWith("+") && !line.startsWith("+++")) additions++;
122
+ else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
123
+ }
124
+ return {
125
+ files,
126
+ additions,
127
+ deletions,
128
+ bytes: Buffer.byteLength(diff, "utf-8")
129
+ };
130
+ }
131
+ function getChangedFiles(diff) {
132
+ const files = [];
133
+ for (const match of diff.matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm)) {
134
+ files.push(match[2]);
135
+ }
136
+ return [...new Set(files)];
137
+ }
138
+ var DIFF_SKIP_PATTERNS = [
139
+ /(?:^|\/)testdata\//,
140
+ /(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.sum|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/,
141
+ /\.mdx?$/
142
+ ];
143
+ function filterEmbeddedDiff(rawDiff) {
144
+ const skippedFiles = [];
145
+ const sections = rawDiff.split(/(?=^diff --git )/m);
146
+ const kept = [];
147
+ for (const section of sections) {
148
+ const match = section.match(/^diff --git a\/(.*?) b\//);
149
+ if (!match) {
150
+ kept.push(section);
151
+ continue;
152
+ }
153
+ const filePath = match[1];
154
+ if (DIFF_SKIP_PATTERNS.some((pattern) => pattern.test(filePath))) {
155
+ skippedFiles.push(filePath);
156
+ } else {
157
+ kept.push(section);
158
+ }
159
+ }
160
+ return { filtered: kept.join(""), skippedFiles };
161
+ }
162
+
34
163
  // src/prompt.ts
35
164
  function buildPrReviewPrompt(opts) {
36
165
  const {
@@ -46,6 +175,9 @@ function buildPrReviewPrompt(opts) {
46
175
  localMode = false,
47
176
  singleTurn = false
48
177
  } = opts;
178
+ const rebasedGitlabReview = platform === "gitlab" && reviewDiffMode === "snapshot";
179
+ const hasPreviousReviewDelta = Boolean(previousReviewSha && !rebasedGitlabReview);
180
+ const previousReviewShaText = previousReviewSha ?? "";
49
181
  let templateText;
50
182
  try {
51
183
  templateText = readFileSync(getTemplatePath("review-task.md"), "utf-8");
@@ -62,44 +194,36 @@ function buildPrReviewPrompt(opts) {
62
194
  if (previousReviewSha && !/^[a-f0-9]{40}$/.test(previousReviewSha)) {
63
195
  throw new Error(`Invalid previous review SHA: ${previousReviewSha}`);
64
196
  }
65
- let prDiffCmd;
66
- let gitDiffCmd;
67
- if (previousReviewSha) {
68
- const separator = reviewDiffMode === "snapshot" ? " " : "...";
69
- prDiffCmd = `git --no-pager diff ${previousReviewSha}${separator}HEAD --name-only`;
70
- gitDiffCmd = `git --no-pager diff ${previousReviewSha}${separator}HEAD`;
71
- logger.info(`${reviewDiffMode === "snapshot" ? "Snapshot" : "Incremental"} review: diffing from ${previousReviewSha.slice(0, 8)} to HEAD`);
72
- } else if (localMode) {
73
- prDiffCmd = `git --no-pager diff ${targetBranch} --name-only`;
74
- gitDiffCmd = `git --no-pager diff ${targetBranch}`;
75
- } else if (platform === "github" || platform === "gitea") {
76
- prDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD --name-only`;
77
- gitDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD`;
78
- } else {
79
- if (diffBaseSha) {
80
- prDiffCmd = `git --no-pager diff ${diffBaseSha} HEAD --name-only`;
81
- gitDiffCmd = `git --no-pager diff ${diffBaseSha} HEAD`;
82
- logger.info(`Using GitLab CI_MERGE_REQUEST_DIFF_BASE_SHA: ${diffBaseSha.slice(0, 8)}`);
83
- } else {
84
- prDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD --name-only`;
85
- gitDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD`;
86
- }
197
+ const diffArgs = getReviewDiffArgs({
198
+ platform,
199
+ targetBranch,
200
+ diffBaseSha,
201
+ previousReviewSha,
202
+ reviewDiffMode,
203
+ localMode
204
+ });
205
+ const gitDiffCmd = `git ${diffArgs.join(" ")}`;
206
+ const prDiffCmd = `${gitDiffCmd} --name-only`;
207
+ if (hasPreviousReviewDelta) {
208
+ logger.info(`${reviewDiffMode === "snapshot" ? "Snapshot" : "Incremental"} review: diffing from ${previousReviewSha?.slice(0, 8)} to HEAD`);
209
+ } else if (rebasedGitlabReview) {
210
+ logger.info("Rebased GitLab review: diffing from the current MR base to HEAD");
87
211
  }
88
212
  let diffExplanation;
89
- if (previousReviewSha) {
90
- diffExplanation = reviewDiffMode === "snapshot" ? `**Snapshot delta mode**: The MR history was rewritten. This directly compares the last reviewed snapshot (commit \`${previousReviewSha.slice(0, 8)}\`) with the current HEAD; it does not imply ancestry.` : `**Incremental mode**: Showing only changes since the last hodor review (commit \`${previousReviewSha.slice(0, 8)}\`).`;
213
+ if (hasPreviousReviewDelta) {
214
+ diffExplanation = reviewDiffMode === "snapshot" ? `**Snapshot delta mode**: The MR history was rewritten. This directly compares the last reviewed snapshot (commit \`${previousReviewShaText.slice(0, 8)}\`) with the current HEAD; it does not imply ancestry.` : `**Incremental mode**: Showing only changes since the last hodor review (commit \`${previousReviewShaText.slice(0, 8)}\`).`;
91
215
  } else if (diffBaseSha) {
92
- diffExplanation = `**GitLab CI Advantage**: This uses GitLab's pre-calculated merge base SHA (\`CI_MERGE_REQUEST_DIFF_BASE_SHA\`), which matches exactly what the GitLab UI shows. This is more reliable than three-dot syntax because it handles force pushes, rebases, and messy histories correctly.`;
216
+ diffExplanation = `**GitLab CI Advantage**: This uses the merge base resolved from the current target branch, which matches the current GitLab MR diff after force pushes and rebases.`;
93
217
  } else {
94
218
  diffExplanation = `**Three-dot syntax** shows ONLY changes introduced on the source branch, excluding changes already on \`${targetBranch}\`.`;
95
219
  }
96
220
  const { contextSection, notesSection, reminderSection } = buildMrSections(mrMetadata);
97
221
  const oneTurn = singleTurn && Boolean(embeddedDiff);
98
222
  let incrementalSection = "";
99
- if (previousReviewSha) {
223
+ if (hasPreviousReviewDelta) {
100
224
  incrementalSection = `## ${reviewDiffMode === "snapshot" ? "Snapshot Delta" : "Incremental Review"} Mode
101
225
 
102
- This is a follow-up review. A previous hodor review was done at commit \`${previousReviewSha.slice(0, 8)}\`. ` + (reviewDiffMode === "snapshot" ? "The branch history was rewritten, so the diff below compares that reviewed snapshot directly with the current HEAD. " : "The diff below shows ONLY changes since that review. ") + "Your job is to review that delta, not the whole MR again.\n\nRules for incremental reviews:\n1. Only report findings introduced or still affected by the new delta.\n2. Do not re-report issues that are already mentioned in existing notes unless the new delta changes the same code and the issue remains newly relevant.\n3. If the delta is small and self-contained, decide from the embedded diff and submit the review without broad repository exploration.\n" + (oneTurn ? "4. No file-inspection tools are available; if a mechanical change like a route/path/string rename leaves a compatibility question you cannot settle from the diff, do not report it.\n" : "4. For mechanical changes like route/path/string renames, verify the direct call sites or tests only when the diff itself leaves a concrete compatibility question.\n") + "5. If the delta does not produce a qualifying finding under the selected review instructions, submit no findings.\n\n";
226
+ This is a follow-up review. A previous hodor review was done at commit \`${previousReviewShaText.slice(0, 8)}\`. ` + (reviewDiffMode === "snapshot" ? "The branch history was rewritten, so the diff below compares that reviewed snapshot directly with the current HEAD. " : "The diff below shows ONLY changes since that review. ") + "Your job is to review that delta, not the whole MR again.\n\nRules for incremental reviews:\n1. Only report findings introduced or still affected by the new delta.\n2. Do not re-report issues that are already mentioned in existing notes unless the new delta changes the same code and the issue remains newly relevant.\n3. If the delta is small and self-contained, decide from the embedded diff and submit the review without broad repository exploration.\n" + (oneTurn ? "4. No file-inspection tools are available; if a mechanical change like a route/path/string rename leaves a compatibility question you cannot settle from the diff, do not report it.\n" : "4. For mechanical changes like route/path/string renames, verify the direct call sites or tests only when the diff itself leaves a concrete compatibility question.\n") + "5. If the delta does not produce a qualifying finding under the selected review instructions, submit no findings.\n\n";
103
227
  }
104
228
  let embeddedDiffSection;
105
229
  let diffFetchInstructions;
@@ -127,7 +251,7 @@ ${changedFiles.map((file) => `- \`${file}\``).join("\n")}
127
251
  startInstruction = "Analyze the diff above and call `submit_review` now, in this turn.";
128
252
  } else {
129
253
  reviewProcessSection = "## Review Process\n\n1. Analyze the embedded diff above thoroughly\n2. Use `grep` to search for patterns when needed\n3. Use bounded line-range reads when surrounding context is essential; avoid reading entire large files\n4. Do not repeat a diff, grep, or read operation whose result is already in context\n5. Submit your review using `submit_review`\n";
130
- startInstruction = previousReviewSha ? "Analyze only the incremental diff provided above. If it is self-contained, submit your review without extra tool calls." : "Analyze the diff provided above, then submit your review using `submit_review`.";
254
+ startInstruction = hasPreviousReviewDelta ? "Analyze only the incremental diff provided above. If it is self-contained, submit your review without extra tool calls." : "Analyze the diff provided above, then submit your review using `submit_review`.";
131
255
  }
132
256
  } else {
133
257
  embeddedDiffSection = "";
@@ -314,6 +438,31 @@ function parseModelString(model) {
314
438
  }
315
439
  return { provider: "anthropic", modelId: trimmed };
316
440
  }
441
+ var BEDROCK_REGIONAL_PREFIXES = ["global", "us", "eu", "apac", "in", "jp", "au", "ca"];
442
+ function stripBedrockRegionalPrefix(modelId) {
443
+ const dot = modelId.indexOf(".");
444
+ if (dot <= 0) return null;
445
+ const prefix = modelId.slice(0, dot).toLowerCase();
446
+ if (!BEDROCK_REGIONAL_PREFIXES.includes(prefix)) return null;
447
+ return modelId.slice(dot + 1);
448
+ }
449
+ function isOpenAiBedrockModel(model) {
450
+ if (model.provider !== "amazon-bedrock") return false;
451
+ return [model.id, model.name].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes("openai"));
452
+ }
453
+ function addOpenAiBedrockReasoning(payload, effort) {
454
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
455
+ const request = payload;
456
+ const existingFields = request.additionalModelRequestFields;
457
+ const additionalModelRequestFields = existingFields && typeof existingFields === "object" && !Array.isArray(existingFields) ? existingFields : {};
458
+ return {
459
+ ...request,
460
+ additionalModelRequestFields: {
461
+ ...additionalModelRequestFields,
462
+ reasoning: { effort }
463
+ }
464
+ };
465
+ }
317
466
  function extractBedrockArnRegion(arn) {
318
467
  const parts = arn.split(":");
319
468
  return parts.length >= 4 && parts[3] ? parts[3] : "us-east-1";
@@ -1486,6 +1635,31 @@ async function detectCiWorkspace(owner, repo) {
1486
1635
  }
1487
1636
  return { path: null, targetBranch: null, diffBaseSha: null };
1488
1637
  }
1638
+ async function resolveGitlabDiffBaseSha(workspace, targetBranch, fallbackSha) {
1639
+ if (!targetBranch) return fallbackSha;
1640
+ try {
1641
+ await exec("git", ["fetch", "--no-tags", "origin", targetBranch], { cwd: workspace });
1642
+ const { stdout } = await exec("git", ["merge-base", "HEAD", "FETCH_HEAD"], { cwd: workspace });
1643
+ const mergeBase = stdout.trim();
1644
+ if (mergeBase) {
1645
+ logger.info(`Calculated current GitLab MR diff base: ${mergeBase.slice(0, 8)}`);
1646
+ return mergeBase;
1647
+ }
1648
+ } catch (err) {
1649
+ logger.warn(`Could not calculate current GitLab MR diff base: ${err}`);
1650
+ }
1651
+ try {
1652
+ const { stdout } = await exec("git", ["merge-base", "HEAD", `origin/${targetBranch}`], { cwd: workspace });
1653
+ const mergeBase = stdout.trim();
1654
+ if (mergeBase) {
1655
+ logger.info(`Calculated GitLab MR diff base from origin/${targetBranch}: ${mergeBase.slice(0, 8)}`);
1656
+ return mergeBase;
1657
+ }
1658
+ } catch {
1659
+ }
1660
+ if (fallbackSha) logger.warn(`Falling back to CI_MERGE_REQUEST_DIFF_BASE_SHA: ${fallbackSha.slice(0, 8)}`);
1661
+ return fallbackSha;
1662
+ }
1489
1663
  function normalizeGitRemotePath(remoteUrl) {
1490
1664
  const trimmed = remoteUrl.trim().replace(/\.git$/, "");
1491
1665
  try {
@@ -1700,11 +1874,18 @@ async function setupWorkspace(opts) {
1700
1874
  try {
1701
1875
  const ci = await detectCiWorkspace(owner, repo);
1702
1876
  let detectedTargetBranch = ci.targetBranch;
1703
- const detectedDiffBaseSha = ci.diffBaseSha;
1877
+ let detectedDiffBaseSha = ci.diffBaseSha;
1704
1878
  let workspace;
1705
1879
  let isTemporary = false;
1706
1880
  if (ci.path) {
1707
1881
  workspace = ci.path;
1882
+ if (platform === "gitlab" && ci.targetBranch) {
1883
+ detectedDiffBaseSha = await resolveGitlabDiffBaseSha(
1884
+ workspace,
1885
+ ci.targetBranch,
1886
+ detectedDiffBaseSha
1887
+ );
1888
+ }
1708
1889
  if (platform === "github" && !detectedTargetBranch) {
1709
1890
  detectedTargetBranch = await getGithubBaseBranch(workspace, prNumber);
1710
1891
  }
@@ -1954,118 +2135,6 @@ function resolveReviewLocations(review, opts) {
1954
2135
  return { review: { ...review, findings }, stats };
1955
2136
  }
1956
2137
 
1957
- // src/review-diff.ts
1958
- var HODOR_REVIEW_SHA_RE = /^\s*<!--\s*hodor:sha:([a-f0-9]{40})\s*-->/i;
1959
- function getHodorReviewShaCandidates(notes) {
1960
- if (!notes || notes.length === 0) return [];
1961
- const candidates = [];
1962
- for (const [index, note] of notes.entries()) {
1963
- const match = note.body?.match(HODOR_REVIEW_SHA_RE);
1964
- if (!match) continue;
1965
- const createdAtMs = Date.parse(note.created_at ?? "");
1966
- candidates.push({
1967
- sha: match[1],
1968
- createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
1969
- index
1970
- });
1971
- }
1972
- candidates.sort((a, b) => {
1973
- if (a.createdAtMs != null && b.createdAtMs != null && a.createdAtMs !== b.createdAtMs) {
1974
- return b.createdAtMs - a.createdAtMs;
1975
- }
1976
- if (a.createdAtMs != null && b.createdAtMs == null) return -1;
1977
- if (a.createdAtMs == null && b.createdAtMs != null) return 1;
1978
- return a.index - b.index;
1979
- });
1980
- return [...new Set(candidates.map(({ sha }) => sha))];
1981
- }
1982
- async function findLatestReviewBase(notes, workspacePath) {
1983
- const candidates = getHodorReviewShaCandidates(notes);
1984
- if (candidates.length === 0) return null;
1985
- logger.info(`Found ${candidates.length} previous Hodor review marker(s)`);
1986
- for (const sha of candidates) {
1987
- try {
1988
- let objectType;
1989
- try {
1990
- ({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
1991
- cwd: workspacePath
1992
- }));
1993
- } catch {
1994
- await exec("git", ["fetch", "--quiet", "origin", sha], {
1995
- cwd: workspacePath
1996
- });
1997
- ({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
1998
- cwd: workspacePath
1999
- }));
2000
- }
2001
- if (objectType.trim() !== "commit") throw new Error("not a commit");
2002
- try {
2003
- await exec("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
2004
- cwd: workspacePath
2005
- });
2006
- return { sha, mode: "incremental" };
2007
- } catch {
2008
- logger.info(
2009
- `Previous review SHA ${sha.slice(0, 8)} is not an ancestor; using snapshot delta`
2010
- );
2011
- return { sha, mode: "snapshot" };
2012
- }
2013
- } catch {
2014
- logger.info(
2015
- `Skipping previous review SHA ${sha.slice(0, 8)}; commit is unavailable`
2016
- );
2017
- }
2018
- }
2019
- return null;
2020
- }
2021
- function getDiffStats(diff) {
2022
- let files = 0;
2023
- let additions = 0;
2024
- let deletions = 0;
2025
- for (const line of diff.split("\n")) {
2026
- if (line.startsWith("diff --git ")) files++;
2027
- else if (line.startsWith("+") && !line.startsWith("+++")) additions++;
2028
- else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
2029
- }
2030
- return {
2031
- files,
2032
- additions,
2033
- deletions,
2034
- bytes: Buffer.byteLength(diff, "utf-8")
2035
- };
2036
- }
2037
- function getChangedFiles(diff) {
2038
- const files = [];
2039
- for (const match of diff.matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm)) {
2040
- files.push(match[2]);
2041
- }
2042
- return [...new Set(files)];
2043
- }
2044
- var DIFF_SKIP_PATTERNS = [
2045
- /(?:^|\/)testdata\//,
2046
- /(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.sum|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/,
2047
- /\.mdx?$/
2048
- ];
2049
- function filterEmbeddedDiff(rawDiff) {
2050
- const skippedFiles = [];
2051
- const sections = rawDiff.split(/(?=^diff --git )/m);
2052
- const kept = [];
2053
- for (const section of sections) {
2054
- const match = section.match(/^diff --git a\/(.*?) b\//);
2055
- if (!match) {
2056
- kept.push(section);
2057
- continue;
2058
- }
2059
- const filePath = match[1];
2060
- if (DIFF_SKIP_PATTERNS.some((pattern) => pattern.test(filePath))) {
2061
- skippedFiles.push(filePath);
2062
- } else {
2063
- kept.push(section);
2064
- }
2065
- }
2066
- return { filtered: kept.join(""), skippedFiles };
2067
- }
2068
-
2069
2138
  // src/review-cache.ts
2070
2139
  import { createHash as createHash2 } from "crypto";
2071
2140
  import { gzipSync, gunzipSync } from "zlib";
@@ -2258,7 +2327,22 @@ async function reviewPr(opts) {
2258
2327
  );
2259
2328
  }
2260
2329
  } else if (!piModel) {
2261
- if (parsed.provider === "openrouter") {
2330
+ if (parsed.provider === "amazon-bedrock") {
2331
+ const inferredBaseModelId = stripBedrockRegionalPrefix(parsed.modelId);
2332
+ const baseModelId = parsed.baseModelId ?? inferredBaseModelId;
2333
+ const baseModel = baseModelId ? modelRuntime.getModel(parsed.provider, baseModelId) : void 0;
2334
+ if (!baseModel) {
2335
+ const hint = parsed.baseModelId ? `Base model "${parsed.baseModelId}" was not found in the installed pi-ai registry.` : `Append "@<base-model-id>" if this is a custom inference profile.`;
2336
+ throw new Error(
2337
+ `Unsupported Bedrock model "${parsed.modelId}". ${hint}`
2338
+ );
2339
+ }
2340
+ const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1";
2341
+ piModel = buildBedrockArnModel({ arn: parsed.modelId, baseModel, region });
2342
+ logger.info(
2343
+ `Regional bedrock model, region: ${region}, capabilities from ${baseModel.id}`
2344
+ );
2345
+ } else if (parsed.provider === "openrouter") {
2262
2346
  piModel = {
2263
2347
  id: parsed.modelId,
2264
2348
  name: parsed.modelId,
@@ -2459,7 +2543,14 @@ async function reviewPr(opts) {
2459
2543
  let diffStats = null;
2460
2544
  let changedFiles = [];
2461
2545
  try {
2462
- const diffArgs = previousReviewSha ? previousReviewBase?.mode === "snapshot" ? ["--no-pager", "diff", previousReviewSha, "HEAD"] : ["--no-pager", "diff", `${previousReviewSha}...HEAD`] : diffBaseSha ? ["--no-pager", "diff", diffBaseSha, "HEAD"] : localMode ? ["--no-pager", "diff", targetBranch] : ["--no-pager", "diff", `origin/${targetBranch}...HEAD`];
2546
+ const diffArgs = getReviewDiffArgs({
2547
+ platform,
2548
+ targetBranch,
2549
+ diffBaseSha,
2550
+ previousReviewSha,
2551
+ reviewDiffMode: previousReviewBase?.mode,
2552
+ localMode
2553
+ });
2463
2554
  const { stdout: rawDiff } = await exec("git", diffArgs, { cwd: workspacePath });
2464
2555
  const { filtered: filteredDiff, skippedFiles } = filterEmbeddedDiff(rawDiff);
2465
2556
  if (skippedFiles.length > 0) {
@@ -2596,14 +2687,29 @@ async function reviewPr(opts) {
2596
2687
  resourceLoader
2597
2688
  });
2598
2689
  activeSession = session;
2599
- if (bedrockTags && parsed.provider === "amazon-bedrock") {
2690
+ const openAiReasoning = thinkingLevel && isOpenAiBedrockModel(piModel) ? thinkingLevel : void 0;
2691
+ if (parsed.provider === "amazon-bedrock" && (bedrockTags || openAiReasoning)) {
2600
2692
  const agent = session.agent;
2601
2693
  const originalStreamFn = agent.streamFn;
2602
2694
  agent.streamFn = (...args) => {
2603
2695
  const options = args[2] ?? {};
2604
- return originalStreamFn(args[0], args[1], { ...options, requestMetadata: bedrockTags });
2696
+ const originalOnPayload = options.onPayload;
2697
+ const onPayload = openAiReasoning ? async (payload, model2) => {
2698
+ const transformed = originalOnPayload ? await originalOnPayload(payload, model2) : void 0;
2699
+ return addOpenAiBedrockReasoning(
2700
+ transformed === void 0 ? payload : transformed,
2701
+ openAiReasoning
2702
+ );
2703
+ } : originalOnPayload;
2704
+ return originalStreamFn(args[0], args[1], {
2705
+ ...options,
2706
+ ...bedrockTags ? { requestMetadata: bedrockTags } : {},
2707
+ ...onPayload ? { onPayload } : {}
2708
+ });
2605
2709
  };
2606
- logger.info(`Bedrock cost allocation tags: ${JSON.stringify(bedrockTags)}`);
2710
+ if (bedrockTags) {
2711
+ logger.info(`Bedrock cost allocation tags: ${JSON.stringify(bedrockTags)}`);
2712
+ }
2607
2713
  }
2608
2714
  let turnCount = 0;
2609
2715
  let toolCallCount = 0;
@@ -2825,4 +2931,4 @@ export {
2825
2931
  postReviewStructured,
2826
2932
  reviewPr
2827
2933
  };
2828
- //# sourceMappingURL=chunk-YKB3BRJM.js.map
2934
+ //# sourceMappingURL=chunk-GISFKKMM.js.map