Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

smart-diff

NPM License GitHub Marketplace Downloads/week Maintainability codecov Mutation testing badge

Generate AI-powered natural-language summaries of git diffs for code review in any git repository.

Supports OpenAI, Anthropic, Google Gemini, Amazon Bedrock, Mistral, Cohere, Groq, xAI, DeepSeek, or any OpenAI-compatible gateway.

Requirements

  • Node.js 22.22.1+
  • An LLM provider credential (see Provider configuration)
  • No git binary required, on any platform — smart-diff reads the git repository directly via @scolladon/tsgit, a pure-TypeScript git implementation with zero native dependencies

Installation

npm install @mcarvin/smart-diff

Provider configuration

smart-diff is "configured" when isLlmProviderConfigured() returns true — i.e. at least one supported provider can be resolved from env vars — or you pass your own llmModelProvider factory. Otherwise summarizeGitDiff / generateSummary throw with LLM_GATEWAY_REQUIRED_MESSAGE.

Selecting a provider

LLM_PROVIDER explicitly selects a provider. When unset, the resolver auto-detects in this order: LLM_BASE_URL/OPENAI_BASE_URLopenai-compatible, OPENAI_API_KEY/LLM_API_KEYopenai, then ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY), MISTRAL_API_KEY, COHERE_API_KEY, GROQ_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY, AWS_ACCESS_KEY_ID/AWS_PROFILEbedrock, and finally OPENAI_DEFAULT_HEADERS/LLM_DEFAULT_HEADERSopenai.

Provider (LLM_PROVIDER) Endpoint Credential env vars Default model
openai OpenAI Chat Completions OPENAI_API_KEY or LLM_API_KEY gpt-4o-mini
openai-compatible Chat Completions at LLM_BASE_URL/OPENAI_BASE_URL LLM_BASE_URL or OPENAI_BASE_URL (required); OPENAI_API_KEY/LLM_API_KEY or custom headers gpt-4o-mini
anthropic Anthropic Messages API ANTHROPIC_API_KEY claude-haiku-4-5-20251001
google Gemini Generative Language API GOOGLE_GENERATIVE_AI_API_KEY or GOOGLE_API_KEY gemini-2.0-flash
bedrock Bedrock Converse API (in-house SigV4 signing) AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY (+ optional AWS_SESSION_TOKEN), or AWS_PROFILE pointing at a profile in ~/.aws/credentials; region from AWS_REGION/AWS_DEFAULT_REGION/~/.aws/config (default us-east-1). SSO, instance-role, and container credentials are not supported — static credentials only. anthropic.claude-3-5-haiku-20241022-v1:0
mistral Mistral Chat Completions API MISTRAL_API_KEY mistral-small-latest
cohere Cohere Chat API (v2) COHERE_API_KEY command-r-08-2024
groq Groq Chat Completions API GROQ_API_KEY llama-3.1-8b-instant
xai xAI Chat Completions API XAI_API_KEY grok-2-latest
deepseek DeepSeek Chat Completions API DEEPSEEK_API_KEY deepseek-chat

LLM_* wins over OPENAI_* where both exist.

Common env vars

Variable Purpose
LLM_PROVIDER Explicit provider id from the table above.
LLM_MODEL Overrides the per-provider default model id.
OPENAI_BASE_URL / LLM_BASE_URL Base URL for an OpenAI-compatible gateway; presence alone auto-selects the openai-compatible provider.
OPENAI_DEFAULT_HEADERS / LLM_DEFAULT_HEADERS JSON object of extra headers merged onto OpenAI / OpenAI-compatible requests (e.g. RBAC tokens, raw Authorization). LLM_* overrides OPENAI_* key-by-key.
LLM_PROVIDER_NAME Display name used when openai-compatible is active (defaults to openai-compatible).
OPENAI_MAX_DIFF_CHARS / LLM_MAX_DIFF_CHARS Max size of unified diff text sent to the model (default ~120k characters).
OPENAI_MAX_TOKENS / LLM_MAX_TOKENS Max completion tokens (default 4000).
LLM_TEMPERATURE Sampling temperature, clamped to 0–2 (default 0.2). Lower = more deterministic; higher = more varied prose.
LLM_MAX_RETRIES Retry count for transient LLM call failures — rate limits, 5xx, network errors (default 2). Set to 0 to disable retries.

Example: native OpenAI

$env:OPENAI_API_KEY = "sk-..."
# Optional: $env:LLM_MODEL = "gpt-4o"

Example: Anthropic Claude

$env:ANTHROPIC_API_KEY = "sk-ant-..."
$env:LLM_MODEL = "claude-3-5-sonnet-latest"   # optional override

Example: company-managed OpenAI-compatible gateway

$env:OPENAI_BASE_URL = "https://llm-gateway.example.com"
$env:OPENAI_DEFAULT_HEADERS = '{"x-company-rbac":"your-rbac-token-here","Authorization":"Bearer sk-your-api-key-here"}'
# LLM_PROVIDER is auto-detected as "openai-compatible" because LLM_BASE_URL/OPENAI_BASE_URL is set.

Example: Google Gemini

$env:GOOGLE_GENERATIVE_AI_API_KEY = "..."
$env:LLM_MODEL = "gemini-2.0-flash"

Usage

Use smart-diff as a library, or as the smart-diff CLI binary that ships with the package via the bin field — no separate install. Provider configuration is identical either way; see Provider configuration. Every option below has a library form (a key in the summarizeGitDiff object) and, where applicable, an equivalent kebab-case CLI flag.

Use as a Library - summarizeGitDiff

import { summarizeGitDiff } from '@mcarvin/smart-diff';

const { summary, usage } = await summarizeGitDiff({
  from: 'origin/main',
  to: 'HEAD',
  cwd: '/path/to/repo', // optional; default process.cwd()
  includeFolders: ['src'],
  excludeFolders: ['node_modules', 'dist'],
  commitMessageExcludeRegexes: ['^\\[bot\\]'],
  commitMessageIncludeRegexes: ['^feat:'], // optional; OR across patterns
  teamName: 'Platform',
  systemPrompt: undefined,   // optional; overrides DEFAULT_GIT_DIFF_SYSTEM_PROMPT
  provider: 'anthropic',     // optional; overrides LLM_PROVIDER env + auto-detection
  model: 'claude-3-5-sonnet-latest', // optional
  maxDiffChars: 120_000,     // optional; also see LLM_MAX_DIFF_CHARS
});
// summary: Markdown string
// usage: LlmUsageReport aggregated across every LLM call — see Token usage reporting below

Use as a CLI

npm install @mcarvin/smart-diff
npx smart-diff origin/main HEAD --team Platform --max-diff-chars 20000
npx smart-diff --help
  • <from> (required) and [to] (default HEAD) can be passed positionally or via --from/--to.
  • Pass --merge-base / -b alongside <from>/--from to resolve it as the merge base of to and from instead of using it directly — e.g. --to develop --from main --merge-base is the tsgit-native equivalent of --to develop --from $(git merge-base develop main), with no local git binary required.
  • Repeatable options (--include, --exclude, --commit-include, --commit-exclude) accept multiple flags.
  • The Markdown summary is printed to stdout; errors go to stderr and exit with code 1.
  • Run smart-diff --help for the full flag reference, or smart-diff --version for the installed version.

Options reference

Core

Option CLI flag Description
from / to <from> [to] / --from / --to Git refs for the range; to defaults to HEAD.
mergeBase --merge-base, -b Resolve from as the merge base of to and from, in-process via tsgit — no local git binary needed.
cwd / git --cwd <path> Working directory path, or inject your own GitClient instance (library only; see Lower-level API).
includeFolders --include <path> Limit diff to these paths relative to repo root (omit for full repo minus excludes).
excludeFolders --exclude <path> Excluded paths, applied client-side to the changed-path list (directory-prefix match), e.g. node_modules.
commitMessageIncludeRegexes --commit-include <regex> If any pattern is non-empty, only commits whose full message matches at least one pattern are kept (after excludes). Case-insensitive.
commitMessageExcludeRegexes --commit-exclude <regex> Drop commits whose message matches any of these patterns.
teamName --team <name> Adds a Team: line to the user payload for the model.
systemPrompt --system-prompt <text> Replaces the default system prompt.
provider --provider <id> LlmProviderId — wins over LLM_PROVIDER env and auto-detection.
model --model <id> Chat model id; overrides LLM_MODEL and the provider default.
llmModelProvider — (library only) () => Promise<ChatModel> — bypass env-based resolution entirely; hand-wire your own ChatModel (required in tests or custom setups).

Token reduction — see Handling large diffs

Option CLI flag Description
maxDiffChars --max-diff-chars <n> Caps unified diff size for the request; see LLM_MAX_DIFF_CHARS.
mapReduce --map-reduce Split an oversized diff into per-file batches instead of hard-truncating it.
contextLines --context-lines <n> Number of context lines around each change (git diff -U<n>). Lower values (1 or 0) are the single biggest token saver on modification-heavy diffs.
ignoreWhitespace --ignore-whitespace Passes -w / --ignore-all-space to git diff so pure-whitespace hunks don't consume tokens. Also applies to --numstat / --name-status so counts stay consistent.
stripDiffPreamble --strip-diff-preamble Removes low-value lines from the unified diff (diff --git, index, mode changes, similarity/rename/copy metadata). --- a/…, +++ b/…, and @@ hunk headers are kept.
maxHunkLines --max-hunk-lines <n> Caps the body of each hunk; anything past the limit is replaced with a single elision marker. The @@ header and DiffSummary totals are preserved.
excludeDefaultNoise --exclude-default-noise Merges the built-in DEFAULT_NOISE_EXCLUDES list (lockfiles, dist, build, out, coverage, node_modules, __snapshots__) into excludeFolders.

Reliability

Option CLI flag Description
maxRetries --max-retries <n> Retry count for transient LLM call failures; see LLM_MAX_RETRIES.

Security

Option CLI flag Description
redactSecrets --redact-secrets Masks likely secrets/credentials in the diff text before it's sent to the LLM — cloud provider keys, VCS/chat tokens, PEM private key blocks, JWTs, Bearer headers, basic-auth URL passwords, and generic KEY=value assignments. Uses DEFAULT_SECRET_PATTERNS unless secretPatterns overrides them.
secretPatterns — (library only) Overrides the built-in secret-detection rules used when redactSecrets is true.

Other

Option CLI flag Description
— (summarizeGitDiff always returns usage) --usage Print the same LlmUsageReport as Token usage reporting to stderr, after the Markdown summary on stdout.
-h, --help Show CLI help.
-v, --version Print the installed version.

Handling large diffs

Reducing tokens

For most repos, the cheapest wins are:

await summarizeGitDiff({
  from: 'origin/main',
  contextLines: 1,          // -U1 cuts 30-60% of tokens on typical diffs
  ignoreWhitespace: true,   // drop pure-whitespace hunks entirely
  stripDiffPreamble: true,  // kill `index`/`mode`/`similarity` lines
  maxHunkLines: 400,        // truncate monster hunks but keep the @@ header
  excludeDefaultNoise: true // skip lockfiles, dist/, coverage/, node_modules/
});

These options only reshape the unified diff text — the structured DiffSummary still reports true file counts and line totals, so the model always sees the full change inventory.

Map-reduce for oversized diffs

By default, a diff over maxDiffChars is hard-truncated — only the first N characters are sent, and a notice is prepended to the summary. Set mapReduce: true to instead split the diff into per-file batches, summarize each batch independently, then synthesize one final summary from the batch summaries:

await summarizeGitDiff({
  from: 'origin/main',
  maxDiffChars: 20_000,
  mapReduce: true, // no-op when the diff already fits within maxDiffChars
});

This costs one extra LLM call per batch plus one reduce call, so it's slower and more expensive than a single request — use it when losing coverage of the tail of a large diff matters more than latency/cost. systemPrompt (if set) applies to the reduce phase only; the map phase always uses DEFAULT_MAP_REDUCE_MAP_SYSTEM_PROMPT.

Token usage reporting

summarizeGitDiff returns the Markdown summary plus token usage aggregated across every LLM call made to produce it — one call by default, or every map-reduce batch plus the reduce call when mapReduce is used:

import { summarizeGitDiff } from '@mcarvin/smart-diff';

const { summary, usage } = await summarizeGitDiff({ from: 'origin/main' });
// usage: { requestCount, inputTokens, outputTokens, totalTokens, cachedInputTokens }

This reports token counts only — no dollar cost estimate, since per-token pricing varies by provider/model and changes over time; a hardcoded price table would drift out of date. Fields are 0 when a provider doesn't report a given figure. generateSummaryWithUsage is the lower-level equivalent of generateSummary.

Injecting your own ChatModel

If you want full control — for example, to hit an in-process mock, add your own middleware/logging, or talk to a provider smart-diff doesn't support directly — pass llmModelProvider. A ChatModel is just an object with a generate method:

import { summarizeGitDiff, type ChatModel } from '@mcarvin/smart-diff';

const myModel: ChatModel = {
  async generate({ system, prompt, temperature, maxOutputTokens }) {
    const res = await fetch('https://my-llm-gateway.example.com/v1/chat', {
      method: 'POST',
      headers: { authorization: `Bearer ${process.env.MY_GATEWAY_KEY}` },
      body: JSON.stringify({ system, prompt, temperature, maxOutputTokens }),
    });
    const data = await res.json();
    return { text: data.text, usage: { inputTokens: data.inputTokens, outputTokens: data.outputTokens } };
  },
};

const { summary } = await summarizeGitDiff({
  from: 'origin/main',
  llmModelProvider: async () => myModel,
});

Diff shape: single range vs per-commit

  • Single unified diff for from..to when no commit-message filters apply and the filtered commit list matches the full log for that range.
  • Concatenated per-commit patches (each commit diffed against its first parent, or the empty tree for a root commit) when you use include/exclude regexes or when the filtered commit list differs in length from the full range (so the diff reflects only the commits that remain).

Lower-level API

The package also exports helpers for building a custom pipeline on top of the same git and LLM behavior:

  • Git: createGitClient(cwd?, timeout?) (async — returns Promise<GitClient>, a live @scolladon/tsgit repository handle; call git.dispose() when done), getRepoRoot, getCommits, getDiff, getDiffSummary, getChangedFiles, filterCommitsByMessageRegexes, buildPathFilterPredicate, matchesAnyPath, renderFileDiff, renderUnifiedDiff, buildFileSummary, mergeFileSummariesByPath, summarizeFiles, shapeUnifiedDiff, redactSecrets, DEFAULT_NOISE_EXCLUDES, DEFAULT_SECRET_PATTERNStimeout is in milliseconds; omit for no timeout
  • AI: generateSummary, generateSummaryWithUsage, resolveLlmMaxDiffChars, resolveLlmMaxRetries, truncateUnifiedDiffForLlm, splitUnifiedDiffIntoFileChunks, groupDiffChunksByBudget
  • Provider resolution: resolveLanguageModel, detectLlmProvider, isLlmProviderConfigured, defaultModelForProvider, resolveLlmBaseUrl, parseLlmDefaultHeadersFromEnv
  • Constants: DEFAULT_GIT_DIFF_SYSTEM_PROMPT, DEFAULT_MAP_REDUCE_MAP_SYSTEM_PROMPT, DEFAULT_MAP_REDUCE_REDUCE_SYSTEM_PROMPT, LLM_GATEWAY_REQUIRED_MESSAGE
  • Types: LlmProviderId, LlmModelProvider, ChatModel, ChatCallOptions, ChatResult, ChatUsage, ResolveLanguageModelOptions, GenerateSummaryInput, SummarizeFlags, LlmUsageReport, DiffFileSummary, DiffSummary, CommitInfo, GitClient, GitDiffRangeQuery, DiffPathFilter, DiffShapingOptions, PathFilterPredicate, RenderedFileDiff, SecretRedactionRule
    • DiffFileSummary.binary?: boolean is true when either blob sniffs as binary; absent for text files

GitHub Action

smart-diff is also published as a GitHub Action (marketplace name smart-diff-ai) — no git binary or Node install needed on the runner, since it runs on node24 from a pre-bundled executable.

- uses: actions/checkout@v7
  with:
    fetch-depth: 0 # full history so `from`/`to` refs resolve

- name: Summarize diff
  id: smart-diff
  uses: mcarvin8/smart-diff@v6
  with:
    from: ${{ github.event.pull_request.base.sha }}
    to: ${{ github.event.pull_request.head.sha }}
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

- name: Comment summary on PR
  uses: peter-evans/create-or-update-comment@v5
  with:
    issue-number: ${{ github.event.pull_request.number }}
    body: ${{ steps.smart-diff.outputs.summary }}

Provider credentials are read from the environment exactly as they are for the CLI — set the relevant secret(s) (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.; see Provider configuration) via the step's env: block. Set provider explicitly if more than one provider's credentials are present in the environment.

Inputs

Input Description Default
from Start ref (older side of the range). Required.
to End ref. HEAD
merge-base Resolve from as the merge base of to and from. false
working-directory Working directory of the git repository. runner checkout dir
include Only include these paths (newline-separated).
exclude Exclude these paths (newline-separated).
commit-include Only keep commits matching these regexes (newline-separated, OR).
commit-exclude Drop commits matching these regexes (newline-separated).
team Team label added to the LLM prompt.
system-prompt Overrides the default system prompt.
provider LLM provider id. auto-detected
model Model id override. provider default
max-diff-chars Caps unified diff size sent to the model.
max-retries Retry count for transient LLM call failures. 2
map-reduce Batch an oversized diff instead of hard-truncating it. false
context-lines Context lines per hunk (git diff -U<n>). 3
ignore-whitespace Drop pure-whitespace hunks. false
strip-diff-preamble Strip diff --git/index/mode noise lines. false
max-hunk-lines Elide hunk bodies longer than this many lines.
exclude-default-noise Skip lockfiles/dist/build/coverage/node_modules. false
redact-secrets Mask likely secrets/credentials before sending the diff to the LLM. false

Outputs

Output Description
summary Markdown summary of the diff.
request-count Number of LLM calls made to produce the summary.
input-tokens Sum of input (prompt) tokens across all calls.
output-tokens Sum of output (completion) tokens across all calls.
total-tokens Sum of total tokens across all calls, as reported by the provider.
cached-input-tokens Sum of cached input tokens read across all calls (0 if unsupported/unreported).

Used By

This package is used downstream by:

  • sf-git-ai-meta-insights — Salesforce metadata wrapper compatible with Salesforce DX projects
  • gitlab-llm-kit — TypeScript toolkit for GitLab REST API access and LLM-powered insights (merge requests, issues, pipelines, security, releases, wiki, and more)

License

MIT

About

Generates AI-powered natural-language summaries of git diffs for code review.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages