SSD Nodes Learn 🎉 VPS $4.99/মাস থেকে
নির্দেশিকা Matt Connorদ্বারা Matt Connor

নিজের VPS-এ AI PR Review Agent সেটআপ করার নিয়ম

আপনার নিজস্ব VPS-এ AI PR রিভিউ এজেন্ট চালানোর সম্পূর্ণ গাইড। এখানে diff-scoped প্রম্পট, ফাইল ফিল্টার, এবং inline কমেন্ট কনফিগার করার পদ্ধতিসহ প্রতিটি PR-এর খরচ কমানোর উপায় দেওয়া হলো।

self-hosted PR review agent যা করে

একটি self-hosted PR review agent হলো আপনার নিজস্ব সার্ভারে চলা একটি ছোট প্রোগ্রাম। এটি একটি pull request (PR)-এর diff পড়ে এবং শুধুমাত্র পরিবর্তিত লাইনগুলো একটি মডেলের কাছে পাঠায়। মডেল থেকে যা ফিরে আসে, তা inline review comment হিসেবে পোস্ট করা হয়। এটি কখনোই আপনার branch checkout করে না এবং pull request-এ স্পর্শ করা হয়নি এমন কোনো ফাইল পড়ে না। এর কাছে শুধুমাত্র একটি মডেল API (application programming interface) key এবং একটি token থাকে, যা দিয়ে শুধুমাত্র comment করা যায়, অন্য কিছু করা সম্ভব নয়।

একটি মডেল diff পড়তে পারে। সেই সমস্যার সমাধান হয়ে গেছে। গুরুত্বপূর্ণ বিষয় হলো diff কোথায় যাচ্ছে এবং key কার কাছে থাকছে। একটি hosted review bot ব্যবহারের অর্থ হলো প্রতিটি private repository থেকে diff আপনার নেটওয়ার্কের বাইরে চলে যায়, তৃতীয় পক্ষের লগে জমা হয় এবং তাদের retention policy-র অধীনে থাকে। আপনার নিজস্ব একটি VPS (virtual private server)-এ, diff GitHub থেকে আপনার সার্ভারে আসে এবং সেখান থেকে মডেল API-তে যায়। আপনি সেই চল্লিশ লাইনের কোড পড়তে পারেন যা নির্ধারণ করে কী পাঠানো হবে।

শুরু করার আগে আপনার যা প্রয়োজন

  • একটি Ubuntu 24.04 চালিত VPS, যেখানে একটি self-hosted GitHub Actions runner আগে থেকেই রিপোজিটরিতে রেজিস্টার করা আছে। রেজিস্টার করার সময় এটিকে pr-review অতিরিক্ত লেবেল দিন, কারণ নিচের ওয়ার্কফ্লোটি সেই লেবেলের ওপর ভিত্তি করেই কাজ করবে।
  • Claude Console থেকে একটি Anthropic API key।
  • এমন একটি রিপোজিটরি যেখানে আপনি নিয়ন্ত্রণ করতে পারেন কারা pull request খুলতে পারবে। একটি প্রাইভেট রিপোজিটরি এক্ষেত্রে সবচেয়ে সহজ সমাধান। পাবলিক রিপোজিটরির ক্ষেত্রে নিচের ফর্ক সেকশনটি দেখুন, তবে সেখানে সমাধানটি কিছুটা জটিল।

VPS-এ reviewer ইনস্টল করা

runner service-টি আপনার তৈরি করা unprivileged account-এ চলে, যা আপনি ./svc.sh install চালানোর সময় তৈরি করেছিলেন। reviewer-টিকে সেই একই account-এর অধীনে ইনস্টল করুন, যাতে sudo ছাড়াই job-টি এটি চালাতে পারে। নিচে runner-এর জায়গায় আপনার account-এর নাম বসান।

sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --version

gh --version কমান্ডটি 2026 সালের আগস্ট মাস অনুযায়ী Ubuntu 24.04-এ gh version 2.45.0 প্রিন্ট করে। 2.20 ভার্সন বা তার পরবর্তী যেকোনো রিলিজেই নিচে ব্যবহৃত --input ফ্ল্যাগটি কাজ করে। Command 'gh' not found মেসেজ আসার অর্থ হলো universe component-টি এনাবল করা নেই, তাই sudo add-apt-repository universe চালিয়ে আবার চেষ্টা করুন।

কী (key) এবং টোকেন (token) যেখানে থাকে

দুটি গোপন তথ্য, দুটি ভিন্ন মেয়াদ। কোনোটিই রিপোজিটরিতে রাখা যাবে না।

ANTHROPIC_API_KEY হলো একটি রিপোজিটরি সিক্রেট, যা Settings-এর অধীনে Secrets and variables, তারপর Actions-এ সেট করা হয়। GitHub এটিকে এনক্রিপ্ট করে এবং রান টাইমে স্টেপের এনভায়রনমেন্টে ইনজেক্ট করে। এটি কখনোই ডিস্কে ফাইল হিসেবে থাকে না এবং গিট হিস্ট্রিতেও থাকে না।

GITHUB_TOKEN ভিন্নভাবে কাজ করে। Actions প্রতিটি জবের জন্য একটি নতুন টোকেন তৈরি করে এবং জব শেষ হলে তা ধ্বংস করে দেয়। এই টোকেন কী করতে পারবে তা ওয়ার্কফ্লোর permissions: ব্লকের মাধ্যমে নির্ধারণ করা হয়, তাই এখানেই মূলত 'least privilege' বা সর্বনিম্ন অনুমতির নীতি কার্যকর হয়:

permissions:
  contents: read
  pull-requests: write

এই টোকেন একটি রিভিউ পোস্ট করতে পারে। এটি কোনো কমিট পুশ করতে, ব্রাঞ্চ মার্জ করতে, ওয়ার্কফ্লো ফাইল এডিট করতে বা অন্য কোনো রিপোজিটরি স্পর্শ করতে পারে না। যে এজেন্ট মন্তব্য করতে পারে সে একজন রিভিউয়ার। যে এজেন্ট পুশ করতে পারে সে একজন কমিটার, এবং কেউ এর জন্য সম্মতি দেয়নি। মডেল কী-এর ক্ষেত্রেও একই সতর্কতা অবলম্বন করুন, কারণ এটি আপনার অ্যাকাউন্টের অর্থ খরচ করে। এই ধরনের সমস্যা সম্পর্কে আরও জানতে দেখুন keeping secrets out of an AI agent's reach

Actions জব লগগুলোতে গোপন স্ট্রিংটিকে *** দিয়ে প্রতিস্থাপন করে। এটি শুধুমাত্র হুবহু স্ট্রিংটির সাথে মিল খুঁজে বের করে, তাই আপনি যদি কোনো কী-কে base64 এনকোড করেন, দুই লাইনে ভাগ করেন বা প্রতিবার একটি করে ক্যারেক্টার প্রিন্ট করেন, তবে তা পরিষ্কারভাবে দেখা যাবে। এমন কোনো ডিবাগ স্টেপ যোগ করবেন না যা এনভায়রনমেন্টের তথ্য ডাম্প করে।

কেন fork থেকে আসা pull request আপনার API key দেখতে পায় না

GitHub-এর নিয়মটি সংক্ষিপ্ত: GITHUB_TOKEN ছাড়া, কোনো workflow যখন কোনো forked repository থেকে ট্রিগার হয়, তখন runner-এর কাছে secrets পাঠানো হয় না। তাই fork থেকে চালানো একটি pull_request আপনার স্ক্রিপ্টকে কোনো ANTHROPIC_API_KEY ছাড়াই শুরু করে, এবং প্রথম API call-টি invalid x-api-key এর কারণে ব্যর্থ হয়।

এর একটি সহজ সমাধান হলো ট্রিগারটিকে pull_request_target এ পরিবর্তন করা, যা base repository-এর context-এ চলে এবং secrets পায়। এখানে এমনটি করবেন না। GitHub-এর নিজস্ব নিরাপত্তা নির্দেশনায় বলা হয়েছে যে, এই workflow-গুলো "privileged, যার অর্থ হলো এগুলো অন্যান্য privileged workflow ট্রিগারের সাথে main branch-এর একই cache শেয়ার করে এবং এগুলোর repository-তে write access ও রেফারেন্স করা secrets-এ প্রবেশাধিকার থাকতে পারে", এবং এর ফলাফল "ব্যবহার করে repository দখল করা সম্ভব"।

একই নির্দেশনায় runner সম্পর্কে স্পষ্টভাবে বলা হয়েছে: "GitHub-এ public repository-এর জন্য self-hosted runner প্রায় কখনোই ব্যবহার করা উচিত নয়, কারণ যেকোনো ব্যবহারকারী repository-এর বিরুদ্ধে pull request খুলতে পারে এবং পরিবেশটিকে compromised করতে পারে।"

এটি দুটি ডিজাইন সিদ্ধান্তকে প্রভাবিত করে। job-টিতে একটি guard থাকে যাতে এটি শুধুমাত্র আপনার নিজের repository-তে push করা branch-এ চলে। এবং workflow-টিতে কোনো actions/checkout ধাপ থাকে না। agent-এর কাছে কখনোই branch-টি disk-এ থাকে না, তাই একটি ক্ষতিকারক pull request শুধুমাত্র টেক্সট হিসেবে মডেলে পাঠানো হয়। এটি আপনার VPS-এ কোনো build script চালাতে পারে না, কারণ আপনার VPS-এ কিছুই এটি রান করে না।

সম্পূর্ণ রিপোজিটরি নয়, শুধুমাত্র diff সংগ্রহ করুন

একটি অনুরোধের মাধ্যমেই আপনি পুরো diff-টিকে plain text হিসেবে পেতে পারেন।

export GH_TOKEN=your_token   # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"

Accept: application/vnd.github.diff media type-টি ব্যবহার করলে response-টি pull request-এর বিবরণ সম্বলিত JSON object-এর পরিবর্তে সরাসরি unified diff হিসেবে আসে এবং gh api সেই body-টিকে কোনো পরিবর্তন ছাড়াই প্রদর্শন করে। আপনার দেখা প্রথম লাইনটি অবশ্যই diff --git a/ দিয়ে শুরু হবে। একটি gh: Not Found (HTTP 404) এর অর্থ হলো, token-টি রিপোজিটরি দেখতে পাচ্ছে না। fine-grained personal token-এর ক্ষেত্রে এর অর্থ সাধারণত এই যে, Pull requests পারমিশনটি দেওয়া হয়নি।

টোকেন খরচ করার আগে ফিল্টার করুন

এই অংশটিই নির্ধারণ করে যে আপনার বটটি মানুষ পড়বে নাকি মিউট করে দেবে। নিচে উল্লিখিত প্রতিটি ফিল্টার মডেল কোনো বাইট দেখার আগেই কার্যকর হয়।

  • পাথ ফিল্টার (Path filters)। লক ফাইল, ভেন্ডর করা ডিরেক্টরি, মিনিফাইড বান্ডেল এবং জেনারেট করা কোড বাদ দিন। package-lock.json-এর ওপর মডেলের কোনো মন্তব্য কেবল নয়েজ তৈরি করে, তাছাড়া ডিফের (diff) অধিকাংশ বাইটই এই ফাইলগুলো থেকে আসে।
  • আকারের সীমাবদ্ধতা (A size cap)। নির্ধারিত সীমার বেশি হলে রিভিউ এড়িয়ে যান এবং সফলভাবে প্রস্থান করুন। 4,000 লাইনের রিফ্যাক্টরের ক্ষেত্রে ষাটটি ভুল অনুমানের পরিবর্তে একটি সৎ মন্তব্য করুন যে এটি স্বয়ংক্রিয়ভাবে রিভিউ করার জন্য অনেক বড়।
  • গুরুত্বের সীমা এবং মন্তব্যের সীমাবদ্ধতা (A severity threshold and a comment cap)। উচ্চ এবং মাঝারি গুরুত্বের সমস্যাগুলো রিপোর্ট করুন, সর্বোচ্চ দশটি পর্যন্ত এবং গুরুত্বের ক্রমানুসারে। এগারো নম্বর মন্তব্য কেউ পড়ে না।

স্ক্রিপ্টটি

এটি /opt/pr-review/review.py হিসেবে সেভ করুন। এটি এনভায়রনমেন্ট থেকে কনফিগারেশন পড়ে, তাই কোড পরিবর্তন না করেই ওয়ার্কফ্লোতে মডেল পরিবর্তন করা সম্ভব।

#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys

import anthropic

REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")

raw_diff = subprocess.run(
    ["gh", "api", f"/repos/{REPO}/pulls/{PR}",
     "-H", "Accept: application/vnd.github.diff"],
    check=True, capture_output=True, text=True,
).stdout

প্রতিটি ফাইলের জন্য ডিফের (diff) অংশ আলাদা করা হলে তবেই পাথ ফিল্টারিং সম্ভব হয়। প্রতিটি লাইনে নম্বর দেওয়া হলে তবেই রিভিউ কমেন্টগুলো সঠিক জায়গায় বসে। GitHub শুধুমাত্র ডিফের অন্তর্ভুক্ত লাইনেই ইনলাইন কমেন্ট গ্রহণ করে, তাই মডেলটিকে অবশ্যই একটি বাস্তব লাইন নম্বর উল্লেখ করতে হয়। নম্বরগুলো দিয়ে দিলে মডেলটি নিজে থেকে নম্বর তৈরি না করে একটি কপি করতে পারে।

def per_file(diff_text):
    """Split a unified diff into one string per file."""
    sections, current = [], []
    for line in diff_text.splitlines():
        if line.startswith("diff --git ") and current:
            sections.append("\n".join(current))
            current = []
        current.append(line)
    if current:
        sections.append("\n".join(current))
    return sections


def annotate(section):
    """Prefix every line that exists in the new file with its line number."""
    out, n, in_hunk = [], 0, False
    for line in section.splitlines():
        if line.startswith("@@"):
            n = int(line.split("+")[1].split(",")[0].split(" ")[0])
            in_hunk = True
            out.append(line)
        elif not in_hunk or line.startswith(("-", "\\")):
            out.append(line)
        else:
            out.append(f"{n}\t{line}")
            n += 1
    return "\n".join(out)


kept = [s for s in per_file(raw_diff)
        if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)

if not payload.strip():
    print("every changed file was filtered out")
    raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
    print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
    raise SystemExit(0)

হাঙ্ক হেডার (hunk header) নম্বর বহন করে। @@ -12,7 +12,9 @@ নির্দেশ করে যে নতুন ফাইলের হাঙ্ক 12 নম্বর লাইন থেকে শুরু হয়েছে, তাই কাউন্টার সেখান থেকে শুরু হয় এবং শুধুমাত্র নতুন যোগ করা ও অপরিবর্তিত লাইনের ক্ষেত্রে বৃদ্ধি পায়। মুছে ফেলা লাইনগুলো নম্বরহীন থাকে, কারণ নতুন ফাইলে সেগুলোর অস্তিত্ব নেই। ব্যাকস্ল্যাশ দিয়ে শুরু হওয়া লাইনগুলোর জন্য গার্ডটি গিট (git) কর্তৃক ফাইলের শেষে লেখা 'no-newline' মার্কারটিকে এড়িয়ে যায়, যা অন্যথায় পরবর্তী প্রতিটি নম্বরকে এক ঘর পিছিয়ে দিত।

উভয় এক্সিটেই 1-এর পরিবর্তে 0 স্ট্যাটাস ব্যবহার করা হয়। একটি ফিল্টার করা বা অতিরিক্ত বড় পুল রিকোয়েস্টের ক্ষেত্রে সবুজ টিক চিহ্ন দেখানো উচিত। মানুষ ব্যবস্থা নিতে পারে না এমন লাল টিক চিহ্নকে উপেক্ষা করা হয়, এবং একবার একটি চেক উপেক্ষা করা হলে বাকিগুলোও উপেক্ষা করা হয়।

SYSTEM = (
    "You review one pull request diff. Every line that exists in the new file is "
    "prefixed with its line number and a tab character. "
    "Report only defects you can see in the lines shown: a crash, a resource leak, "
    "a security mistake, a wrong boundary condition, a broken contract with code "
    "that is visible in this diff. Do not comment on style, naming or formatting. "
    "Do not guess about code you cannot see. Leave out anything you are not "
    "certain about. An empty findings list is a normal and common answer. "
    'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
    '"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
    "Every line number must be one you can see in the left column of that file."
)

client = anthropic.Anthropic()
message = client.messages.create(
    model=MODEL,
    max_tokens=2000,
    system=SYSTEM,
    messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
      f"out={message.usage.output_tokens}", file=sys.stderr)

text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]

if not findings:
    print("nothing above the severity threshold; posting no comment")
    raise SystemExit(0)

review = {
    "event": "COMMENT",
    "body": f"Automated review of the changed lines. {len(findings)} finding(s).",
    "comments": [
        {"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
         "body": f"**{f['severity']}** {f['comment']}"}
        for f in findings
    ],
}
subprocess.run(
    ["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
    input=json.dumps(review), text=True, check=True,
)

এই ব্লকের তিনটি বিষয় অত্যন্ত গুরুত্বপূর্ণ। JSON-কে প্রথম { এবং শেষ }-এর মধ্যবর্তী অংশ থেকে স্লাইস করা হয়, কারণ মডেল অনেক সময় তার উত্তরকে কোড ফেন্সের ভেতর রাখে এবং json.loads সেই ফেন্সের কারণে আটকে যায়। path থেকে শুরুর b/ সরিয়ে ফেলা হয়, কারণ এই প্রিফিক্সটি ডিফের হেডার থেকে আসে এবং GitHub-এর রিপোজিটরি-রিলেটিভ পাথ প্রয়োজন হয়। এবং --input - পুরো রিভিউটিকে একটি API কল হিসেবে পাঠায়, ফলে দশটি নোটিফিকেশন না পাঠিয়ে একটি নোটিফিকেশনের মাধ্যমেই দশটি ফলাফল পৌঁছে যায়।

রিপোর্ট করার মতো কিছু না থাকলে স্ক্রিপ্টটি কিছুই পোস্ট করে না। যে বট প্রতিটি পুল রিকোয়েস্টে "no issues found" লেখে, তা মানুষকে সেই মেসেজ এড়িয়ে যেতে অভ্যস্ত করে তোলে; ফলে গুরুত্বপূর্ণ মেসেজগুলোও তারা এড়িয়ে যায়।

এটিকে ওয়ার্কফ্লোতে যুক্ত করুন

এটিকে .github/workflows/pr-review.yml হিসেবে সেভ করুন:

name: pr-review

on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths-ignore:
      - '**.md'
      - 'docs/**'

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: pr-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
    runs-on: [self-hosted, linux, pr-review]
    steps:
      - name: Review the changed lines
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REVIEW_MODEL: claude-haiku-4-5-20251001
          MIN_SEVERITY: medium
        run: /opt/pr-review/venv/bin/python /opt/pr-review/review.py

GITHUB_REPOSITORY সেই env: ব্লকে নেই কারণ Actions প্রতিটি জবের জন্য এটি আগেই সেট করে রাখে। concurrency গ্রুপটি বিলের জন্য গুরুত্বপূর্ণ: এটি ছাড়া, একটি ব্রাঞ্চে তিনটি দ্রুত ফিক্স পুশ করলে তিনটি পূর্ণাঙ্গ রিভিউ রান হবে এবং আপনাকে তিনটির জন্যই মূল্য পরিশোধ করতে হবে, কিন্তু এটি থাকলে শুধুমাত্র সর্বশেষটি কার্যকর থাকে।

if: লাইনটি দুটি কাজ করে। এর প্রথমার্ধ ফর্ক থেকে আসা পুল রিকোয়েস্টগুলোকে বাদ দেয়, যা কোনো কি (key) ছাড়া এমনিতেই ব্যর্থ হতো। দ্বিতীয়ার্ধ আপনার টিমকে একটি অফ সুইচ দেয়: কোনো পুল রিকোয়েস্টে no-ai-review লেবেল যোগ করলে জবটি রান হবে না।

একটি পুল রিকোয়েস্ট ওপেন করুন এবং দেখুন কী ঘটে:

gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --comments

যে রান কয়েক সেকেন্ডের মধ্যে লগে nothing above the severity threshold; posting no comment নিয়ে শেষ হয়, সেটি সঠিকভাবে কাজ করছে। একটি ছোট এবং পরিচ্ছন্ন পুল রিকোয়েস্টের ক্ষেত্রে এটিই প্রত্যাশিত ফলাফল।

স্বয়ংক্রিয় পুল রিকোয়েস্ট রিভিউয়ের খরচ কত?

diff হলো ইনপুটের প্রায় পুরোটাই, তাই diff-এর আকারই খরচ নির্ধারণ করে। নিচে 500 লাইনের একটি diff এবং সিস্টেম প্রম্পট পরিমাপ করে দেখানো হলো, যা অনুমানের পরিবর্তে টোকেন কাউন্টিং এন্ডপয়েন্ট ব্যবহার করে গণনা করা হয়েছে।

ChartTokens for one 500 line pull request diff, measured August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5",
    "input_tokens": "8,000",
    "output_tokens": "1,200"
  },
  {
    "label": "Sonnet 5",
    "input_tokens": "10,400",
    "output_tokens": "1,560"
  },
  {
    "label": "Opus 5",
    "input_tokens": "10,400",
    "output_tokens": "1,560"
  }
]

এই diff-টি Haiku 4.5 মডেলে 8,000 ইনপুট টোকেন এবং Sonnet 5 মডেলে 10,400 টোকেন নিয়েছে। টেক্সট একই, কিন্তু কাউন্ট ভিন্ন। 4.7 সংস্করণ থেকে Claude মডেলগুলো একটি নতুন টোকেনাইজার ব্যবহার করে, যা একই ইনপুটের জন্য প্রায় 30% বেশি টোকেন তৈরি করে; Anthropic তাদের প্রাইসিং পেজে এটি উল্লেখ করেছে। তাই যখনই কোনো নতুন মডেলের সাথে পুরনো মডেলের প্রতি মিলিয়ন টোকেনের খরচের তুলনা করবেন, তখন এই বিষয়টি মাথায় রাখবেন।

আগস্ট 2026 অনুযায়ী তালিকাভুক্ত মূল্য: Haiku 4.5-এর প্রতি মিলিয়ন ইনপুট টোকেনের দাম $1 এবং আউটপুট টোকেনের দাম $5। Sonnet 5-এর দাম 31 আগস্ট 2026 পর্যন্ত প্রারম্ভিক মূল্যে প্রতি মিলিয়ন ইনপুট টোকেন $2 এবং আউটপুট $10, এরপর তা হবে যথাক্রমে $3 এবং $15। Opus 5-এর ক্ষেত্রে এই দাম প্রতি মিলিয়ন ইনপুট টোকেন $5 এবং আউটপুট $25।

ChartCost of that one review at list prices, August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5",
    "cost_per_pr_cents": 1.4,
    "cost_200_prs_usd": "2.80"
  },
  {
    "label": "Sonnet 5",
    "cost_per_pr_cents": 3.64,
    "cost_200_prs_usd": "7.28"
  },
  {
    "label": "Opus 5",
    "cost_per_pr_cents": 9.1,
    "cost_200_prs_usd": "18.20"
  }
]

এর মানে Haiku 4.5 মডেলে প্রতি পুল রিকোয়েস্টের খরচ 1.4 সেন্ট এবং Opus 5 মডেলে 9.1 সেন্ট। যে টিম মাসে 200টি পুল রিকোয়েস্ট মার্জ করে, তাদের Haiku 4.5 মডেলে খরচ হবে প্রায় $2.80, Sonnet 5 মডেলে $7.28, অথবা Opus 5 মডেলে $18.20। 1 সেপ্টেম্বর 2026 থেকে Sonnet 5-এর খরচের হিসাব 1.5 গুণ হবে।

দুটি বিষয় প্রকৃত বিলকে এই অনুমানের চেয়ে বাড়িয়ে দেয়। synchronize প্রতিটি পুশেই রিভিউ ট্রিগার করে, তাই একটি সক্রিয় ব্রাঞ্চে আটটি পুশ থাকলে আটবার রিভিউয়ের খরচ হয় এবং কনকারেন্সি রুল শুধুমাত্র তখনই সাহায্য করে যখন পুশগুলো খুব কাছাকাছি সময়ে আসে। এই হিসাবগুলো পাথ ফিল্টার সঠিকভাবে কাজ করছে ধরে নিয়ে করা হয়েছে: একটি ফিল্টার না করা lock file একাই ইনপুট দ্বিগুণ করে দিতে পারে।

প্রম্পট ক্যাশিং এখানে কোনো কাজে আসে না। ক্যাশ করা প্রিফিক্সটিকে প্রতিটি কলের জন্য বাইট-আইডেন্টিক্যাল হতে হয়, কিন্তু diff প্রতিবারই ভিন্ন হয়। সিস্টেম প্রম্পটটিই একমাত্র স্থিতিশীল অংশ, এবং এটি সর্বনিম্ন ক্যাশেবল দৈর্ঘ্যের অনেক নিচে থাকে। সাধারণ নিয়মের জন্য দেখুন কখন প্রম্পট ক্যাশিং সাশ্রয়ী হয়, এবং উপরের তিনটি মডেলের মধ্যে কোনটি বেছে নেবেন তা জানতে দেখুন কোন কাজের জন্য কোন Claude মডেল ব্যবহার করবেন

এটি চালু করার আগে আপনার নিজের diff পরিমাপ করুন

payload তৈরি হওয়ার পর এই লাইনটি যোগ করুন, তারপর গত মাসের কয়েকটি পুল রিকোয়েস্টের বিপরীতে ম্যানুয়ালি স্ক্রিপ্টটি চালান:

print(client.messages.count_tokens(
    model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)

কাউন্ট এন্ডপয়েন্ট মডেলটি রান করে না, তাই এটি কোনো ইনপুট বা আউটপুট টোকেন খরচ করে না এবং এটি আপনার নির্দিষ্ট করা মডেলের টোকেনাইজার ব্যবহার করে। আপনার নিজের রিপোজিটরি থেকে দশটি বাস্তব পুল রিকোয়েস্টের ওপর এটি চালান এবং গড় মানের পরিবর্তে মিডিয়ান মান নিন, যাতে একটি বিশাল মাইগ্রেশন আপনার অনুমানকে ভুল পথে পরিচালিত না করে।

কেন রিভিউ বট মিউট করা হয় এবং তা কীভাবে এড়ানো যায়

দুটি আচরণ এই বটগুলোর প্রতি আস্থা নষ্ট করে, এবং উপরের কোডে উভয় সমস্যারই সমাধান রয়েছে।

একসাথে সবকিছু রিভিউ করা। যে বট চল্লিশটি মন্তব্য করে, তার একটিও কেউ পড়ে না। severity threshold এবং দশটি মন্তব্যের সীমা ভদ্রতার খাতিরে নয়, বরং এগুলোই আসল ত্রুটিগুলোকে দৃশ্যমান রাখে। truncation-এর আগে severity অনুযায়ী সাজালে, সীমা অতিক্রমকারী কম গুরুত্বপূর্ণ ত্রুটিগুলো বাদ পড়ে, এলোমেলো দশটি মন্তব্য বাদ পড়ার চেয়ে এটি কার্যকর।

যা যাচাই করা সম্ভব নয়, সে বিষয়ে নিশ্চিতভাবে মন্তব্য করা। এই আচরণটিই ইঞ্জিনিয়ারদের বটটি চিরতরে বন্ধ করে দিতে বাধ্য করে। 40,000 লাইনের কোডবেসের 200 লাইন দেখানো হলে একটি মডেল তবুও বলতে পারে "এটি redis_client.py-এর cache invalidation নষ্ট করে", অথচ সে ফাইলটি সে কখনোই দেখেনি। system prompt-এ সহজ ভাষায় নির্দেশনা দিন: শুধুমাত্র প্রদর্শিত লাইনগুলোতে দৃশ্যমান ত্রুটিগুলো রিপোর্ট করুন এবং যে বিষয়ে নিশ্চিত নন তা এড়িয়ে যান। সাধারণভাবে নির্ভুল হওয়ার কথা বলার চেয়ে সরাসরি ব্যর্থতার কথা উল্লেখ করা বেশি কার্যকর। মডেলকে এটি জানানো যে কোনো ফলাফল না পাওয়া স্বাভাবিক, তা ছোট পরিবর্তনের ক্ষেত্রে অহেতুক মন্তব্য করা থেকে তাকে বিরত রাখে।

রিভিউটি COMMENT হিসেবে পোস্ট করুন, কখনোই REQUEST_CHANGES হিসেবে নয়। একটি মডেলের মতামত কোনো merge আটকে দিতে পারবে না; আর যদি তা পারে, তবে ডেডলাইনের চাপে থাকা কেউ বটের সাথে তর্ক করার চেয়ে পুরো workflow-টিই সরিয়ে ফেলবে।

ব্যর্থতার ধরন এবং যে বার্তাগুলো আপনি দেখবেন

রিভিউ পোস্ট করার সময় HTTP 422 ত্রুটি। gh প্রিন্ট করে gh: Unprocessable Entity (HTTP 422) এবং রেসপন্স বডিতে ফিল্ডের নাম উল্লেখ থাকে: Pull request review thread line must be part of the diff। GitHub সেই কমেন্টটি অ্যাঙ্কর করতে পারে না। সাধারণত মডেলের তৈরি করা ভুল লাইন নম্বর, অথবা এমন একটি path যাতে এখনও b/ প্রিফিক্স রয়ে গেছে, কিংবা মুছে ফেলা লাইনে কমেন্ট করার কারণে এমনটি হয়। মুছে ফেলা লাইনের ক্ষেত্রে side-কে RIGHT-এর পরিবর্তে LEFT সেট করতে হবে। পোস্ট করার আগে রিভিউ JSON প্রিন্ট করুন এবং একটি কমেন্ট ডিফের সাথে মিলিয়ে দেখুন।

মডেল API থেকে invalid x-api-key প্রথম messages.create কল করার সময় এই ধাপটি ব্যর্থ হয়। হয় রিপোজিটরিতে ANTHROPIC_API_KEY সিক্রেট সেট করা নেই, অথবা পুল রিকোয়েস্টটি কোনো ফর্ক থেকে এসেছে বলে Actions কোনো সিক্রেট পাঠায়নি। if: লাইনের ফর্ক গার্ডের এটি এড়িয়ে যাওয়ার কথা, তাই প্রথমে সেই লাইনটি পরীক্ষা করুন।

gh: Resource not accessible by integration (HTTP 403) জব টোকেন পুল রিকোয়েস্টে লিখতে পারছে না। permissions: ব্লকে pull-requests: write যোগ করুন। যদি এটি আগে থেকেই থাকে, তবে Settings-এ গিয়ে Actions এবং তারপর General সেকশনে দেখুন; সেখানে কোনো অর্গানাইজেশন পলিসি ওয়ার্কফ্লো টোকেনের অনুমতির সীমা নির্ধারণ করে দিতে পারে।

json.decoder.JSONDecodeError মডেলটি পার্সযোগ্য JSON রিটার্ন করেনি। এর সাধারণ কারণ হলো রেসপন্সটি টোকেন সীমার মধ্যে আটকে গেছে এবং অবজেক্টের মাঝপথে থেমে গেছে। লগ লাইনে ঠিক এই কারণে stop_reason প্রিন্ট হয়: max_tokens মান আসার অর্থ হলো max_tokens বাড়াতে হবে অথবা MAX_COMMENTS কমাতে হবে।

ওয়ার্কফ্লো কখনোই রান করে না। পুল রিকোয়েস্টের জন্য gh run list কিছুই দেখায় না। নিশ্চিত করুন যে paths-ignore কোনো পরিবর্তিত ফাইল ফিল্টার করে দেয়নি। এরপর ফর্ক গার্ড এবং লেবেল গার্ড পরীক্ষা করুন। সবশেষে VPS-এ sudo systemctl status 'actions.runner.*' চালিয়ে দেখুন রানার সচল আছে কি না। রানার অফলাইন থাকলে জবটি কিউতে আটকে থাকে এবং পুল রিকোয়েস্টে কোনো ত্রুটির বার্তা দেখা যায় না।

প্রতিটি রিভিউ খালি ফিরে আসে। একটি রানের জন্য MIN_SEVERITY-কে low সেট করুন। যদি ফলাফল দেখা যায়, তবে থ্রেশহোল্ড সঠিকভাবে কাজ করছে। যদি কিছুই না আসে, তবে payload প্রিন্ট করুন এবং নিশ্চিত করুন যে ফিল্টারগুলো পুরো ডিফ মুছে ফেলেনি।

অন্যান্য এজেন্টের পাশাপাশি এটি চালানো

রিভিউয়ারটি আকারে ছোট, তাই এটিকে এমন একটি বক্সে রাখা প্রলুব্ধকর যেখানে ইতিমধ্যে সবকিছু চলছে। যদি রিপোজিটরি গুরুত্বপূর্ণ হয়, তবে এটিকে আলাদা রাখুন। এই প্রসেসটি এমন একটি টোকেন ধারণ করে যা আপনার কোডে মন্তব্য করতে পারে এবং এমন একটি কি (key) ধারণ করে যা আপনার অর্থ ব্যয় করতে পারে। একটি self-hosted রানার মূলত এমন একটি জায়গা যেখানে ওয়ার্কফ্লো কোড এক্সিকিউট হয়। একটি ডেডিকেটেড আনপ্রিভিলেজড অ্যাকাউন্ট যার কোনো sudo অধিকার নেই এবং এমন একটি হোস্ট যেখানে অন্য কিছু চলে না, সেটিই হলো বেসলাইন। আপনি যদি এমন ইন্টারঅ্যাক্টিভ এজেন্টও চালান যা কোড চেক আউট করে, তবে প্রতি এজেন্টের জন্য একটি ডিসপোজেবল VM ব্যবহার করাই হলো সঠিক পদ্ধতি, এবং একটি VPS-এ কোডিং এজেন্ট চালানো সাধারণ সেটআপটি কভার করে। যদি Anthropic API আপনার কাছে নতুন হয়, তবে একটি VPS-এ প্রথম Claude API অ্যাপ তৈরি করা এখান থেকে শুরু করার জন্য একটি ছোট জায়গা।

FAQ

একটি AI PR review agent-এর কি আমার repository-তে write access প্রয়োজন?

না। একটি review পোস্ট করার জন্য এর pull-requests: write এবং diff সংগ্রহ করার জন্য contents: read প্রয়োজন। এটিই সম্পূর্ণ তালিকা, এবং আপনি এটি workflow-এর permissions: ব্লকে সেট করেন, যা প্রতি-job GITHUB_TOKEN কী করতে পারবে তা সীমাবদ্ধ করে। এই দুটি লাইন থাকলে agent একটি pull request-এ মন্তব্য করতে পারে কিন্তু কোনো commit push করতে বা branch merge করতে পারে না। REQUEST_CHANGES-এর পরিবর্তে event: COMMENT ব্যবহার করে review পোস্ট করুন, যাতে এটি কোনো merge-কে বাধা দিতে না পারে।

আমার review মন্তব্য কেন HTTP 422 এর সাথে ব্যর্থ হয়?

GitHub শুধুমাত্র pull request diff-এর অন্তর্ভুক্ত লাইনেই inline review মন্তব্য গ্রহণ করে, এবং যখন তা হয় না তখন Pull request review thread line must be part of the diff রিটার্ন করে। নিশ্চিত করুন যে path হলো repository-relative এবং এতে diff header থেকে কোনো b/ প্রিফিক্স নেই, এবং লাইন নম্বরটি সেই ফাইলের একটি hunk-এর ভেতরে রয়েছে। একটি নতুন বা অপরিবর্তিত লাইনের জন্য side অবশ্যই RIGHT হতে হবে এবং একটি মুছে ফেলা লাইনের জন্য LEFT হতে হবে। মডেলে পাঠানোর আগে diff-এর প্রতিটি লাইনের শুরুতে নতুন ফাইলের লাইন নম্বর যুক্ত করাই মডেলকে ভুল নম্বর তৈরি করা থেকে বিরত রাখে।

আমি কি fork থেকে আসা pull request সহ একটি public repository-তে এটি চালাতে পারি?

এই ডিজাইনে এটি সম্ভব নয়। GitHub একটি fork থেকে ট্রিগার হওয়া workflow-এ secrets পাঠায় না, তাই মডেল কী (key) অনুপস্থিত থাকে এবং রান ব্যর্থ হয়। GitHub আরও বলে যে self-hosted runner "public repository-এর জন্য কখনোই ব্যবহার করা উচিত নয়", কারণ যে কেউ এমন একটি pull request খুলতে পারে যা আপনার মেশিনে কোড রান করাবে। একটি public project-এর জন্য, reviewer-কে শুধুমাত্র repository-তে push করা branch-এর মধ্যে সীমাবদ্ধ রাখুন, যা if: গার্ড করে থাকে, অথবা review ধাপটিকে একটি GitHub-hosted runner-এ সরিয়ে নিন এবং মেনে নিন যে diff আপনার নিজস্ব পরিকাঠামোর বাইরে চলে যাচ্ছে।

pull request review-এর জন্য আমার কোন মডেল ব্যবহার করা উচিত?

Haiku 4.5 দিয়ে শুরু করুন। একটি নির্দিষ্ট ত্রুটির তালিকার বিপরীতে একটি সীমাবদ্ধ diff পড়া কোনো কঠিন যুক্তিনির্ভর সমস্যা নয়, এবং সবচেয়ে সস্তা মডেলটি মাসিক বিল এমন একটি সংখ্যায় রাখে যা নিয়ে কারো আপত্তি থাকে না। যদি দেখেন এটি আপনার ভাষা বা framework-এর প্রকৃত বাগগুলো ধরতে পারছে না, তবে Sonnet 5-এ উন্নীত হোন এবং অনুমানের পরিবর্তে তা পরিমাপ করুন। Opus 5 এই তিনটির মধ্যে প্রতি pull request-এ অনেক বেশি ব্যয়বহুল, যা প্রতিটি feature branch-এর প্রতিটি push-এর চেয়ে release branch-এ ব্যবহার করা বেশি যুক্তিযুক্ত।