@mcarvin/smart-diff 1.0.5 → 2.0.0

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
@@ -3,14 +3,16 @@
3
3
  [![NPM](https://img.shields.io/npm/v/@mcarvin/smart-diff.svg?label=smart-diff)](https://www.npmjs.com/package/@mcarvin/smart-diff)
4
4
  [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://raw.githubusercontent.com/mcarvin8/smart-diff/main/LICENSE.md)
5
5
  [![Downloads/week](https://img.shields.io/npm/dw/@mcarvin/smart-diff.svg)](https://npmjs.org/package/@mcarvin/smart-diff)
6
+ [![Maintainability](https://qlty.sh/gh/mcarvin8/projects/smart-diff/maintainability.svg)](https://qlty.sh/gh/mcarvin8/projects/smart-diff)
6
7
  [![codecov](https://codecov.io/gh/mcarvin8/smart-diff/graph/badge.svg?token=H3ZWAGG7S9)](https://codecov.io/gh/mcarvin8/smart-diff)
7
8
 
8
- TypeScript library that turns a **git revision range** into a **Markdown summary** using an OpenAI-compatible Chat Completions API. It uses [`simple-git`](https://github.com/steveukx/git-js) to read the repo, respects **path includes/excludes** and **commit message include/exclude regexes**, and sends commits, paths, structured diff stats, and unified diff text to the model.
9
+ TypeScript library that turns a **git revision range** into a **Markdown summary** using any LLM provider supported by the [Vercel AI SDK](https://sdk.vercel.ai) — OpenAI, Anthropic, Google Gemini, Amazon Bedrock, Mistral, Cohere, Groq, xAI, DeepSeek, or any OpenAI-compatible gateway. It uses [`simple-git`](https://github.com/steveukx/git-js) to read the repo, respects **path includes/excludes** and **commit message include/exclude regexes**, and sends commits, paths, structured diff stats, and unified diff text to the model.
9
10
 
10
11
  ## Requirements
11
12
 
12
13
  - **Node.js** 20+
13
- - [Git Bash](https://git-scm.com/install/)
14
+ - An LLM provider credential (see [Provider configuration](#provider-configuration))
15
+ - [Git](https://git-scm.com/) on the `PATH`
14
16
 
15
17
  ## Installation
16
18
 
@@ -18,25 +20,70 @@ TypeScript library that turns a **git revision range** into a **Markdown summary
18
20
  npm install @mcarvin/smart-diff
19
21
  ```
20
22
 
21
- ## LLM configuration
23
+ `@ai-sdk/openai` and `@ai-sdk/openai-compatible` ship as direct dependencies. Every other provider (`@ai-sdk/anthropic`, `@ai-sdk/google`, `@ai-sdk/amazon-bedrock`, `@ai-sdk/mistral`, `@ai-sdk/cohere`, `@ai-sdk/groq`, `@ai-sdk/xai`, `@ai-sdk/deepseek`) is declared as an **optional peer** and only needs to be installed when you actually use that provider. If the package is missing, smart-diff throws a clear error telling you which one to install.
22
24
 
23
- The library is considered “configured” when `shouldUseLlmGateway()` is true: API key, base URL, and/or JSON default headers are set. Otherwise `summarizeGitDiff` / `generateSummary` throw with `LLM_GATEWAY_REQUIRED_MESSAGE` unless you pass **`openAiClientProvider`**.
25
+ ## Provider configuration
26
+
27
+ smart-diff is "configured" when [`isLlmProviderConfigured()`](#lower-level-api) 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`.
28
+
29
+ ### Selecting a provider
30
+
31
+ `LLM_PROVIDER` explicitly selects a provider. When unset, the resolver auto-detects in this order: `LLM_BASE_URL`/`OPENAI_BASE_URL` → `openai-compatible`, `OPENAI_API_KEY`/`LLM_API_KEY` → `openai`, 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`, and finally `OPENAI_DEFAULT_HEADERS`/`LLM_DEFAULT_HEADERS` → `openai`.
32
+
33
+ | Provider (`LLM_PROVIDER`) | Package | Credential env vars | Default model |
34
+ |---|---|---|---|
35
+ | `openai` | `@ai-sdk/openai` | `OPENAI_API_KEY` or `LLM_API_KEY` | `gpt-4o-mini` |
36
+ | `openai-compatible` | `@ai-sdk/openai-compatible` | `LLM_BASE_URL` or `OPENAI_BASE_URL` (required); `OPENAI_API_KEY`/`LLM_API_KEY` or custom headers | `gpt-4o-mini` |
37
+ | `anthropic` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-latest` |
38
+ | `google` | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` or `GOOGLE_API_KEY` | `gemini-2.0-flash` |
39
+ | `bedrock` | `@ai-sdk/amazon-bedrock` | Standard AWS credential chain (env / profile / role) | `anthropic.claude-3-5-haiku-20241022-v1:0` |
40
+ | `mistral` | `@ai-sdk/mistral` | `MISTRAL_API_KEY` | `mistral-small-latest` |
41
+ | `cohere` | `@ai-sdk/cohere` | `COHERE_API_KEY` | `command-r-08-2024` |
42
+ | `groq` | `@ai-sdk/groq` | `GROQ_API_KEY` | `llama-3.1-8b-instant` |
43
+ | `xai` | `@ai-sdk/xai` | `XAI_API_KEY` | `grok-2-latest` |
44
+ | `deepseek` | `@ai-sdk/deepseek` | `DEEPSEEK_API_KEY` | `deepseek-chat` |
45
+
46
+ > `LLM_*` wins over `OPENAI_*` where both exist.
47
+
48
+ ### Common env vars
24
49
 
25
50
  | Variable | Purpose |
26
- |----------|---------|
27
- | `OPENAI_API_KEY` or `LLM_API_KEY` | API key (`LLM_*` wins over `OPENAI_*` where both exist). |
28
- | `OPENAI_BASE_URL` or `LLM_BASE_URL` | Base URL for an OpenAI-compatible gateway (`LLM_*` overrides). |
29
- | `OPENAI_DEFAULT_HEADERS` / `LLM_DEFAULT_HEADERS` | JSON object of extra headers; `LLM_*` merges on top of `OPENAI_*`. Can supply `Authorization` (e.g. raw `sk-…`) when no env key is set. |
30
- | `LLM_MAX_DIFF_CHARS` | Max size of unified diff text sent to the model (default ~120k characters). |
31
- | `LLM_MAX_TOKENS` or `OPENAI_MAX_TOKENS` | Max completion tokens (default 4000). |
51
+ |---|---|
52
+ | `LLM_PROVIDER` | Explicit provider id from the table above. |
53
+ | `LLM_MODEL` | Overrides the per-provider default model id. |
54
+ | `OPENAI_BASE_URL` / `LLM_BASE_URL` | Base URL for an OpenAI-compatible gateway; presence alone auto-selects the `openai-compatible` provider. |
55
+ | `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. |
56
+ | `LLM_PROVIDER_NAME` | Display name used when `openai-compatible` is active (defaults to `openai-compatible`). |
57
+ | `OPENAI_MAX_DIFF_CHARS` / `LLM_MAX_DIFF_CHARS` | Max size of unified diff text sent to the model (default ~120k characters). |
58
+ | `OPENAI_MAX_TOKENS` / `LLM_MAX_TOKENS` | Max completion tokens (default 4000). |
59
+
60
+ ### Example: native OpenAI
61
+
62
+ ```powershell
63
+ $env:OPENAI_API_KEY = "sk-..."
64
+ # Optional: $env:LLM_MODEL = "gpt-4o"
65
+ ```
32
66
 
33
- The client is created with the official [`openai`](https://www.npmjs.com/package/openai) SDK via `createOpenAiLikeClient()`; use a compatible endpoint and model ID for your provider.
67
+ ### Example: Anthropic Claude
68
+
69
+ ```powershell
70
+ $env:ANTHROPIC_API_KEY = "sk-ant-..."
71
+ $env:LLM_MODEL = "claude-3-5-sonnet-latest" # optional override
72
+ ```
34
73
 
35
- Example using a company-managed OpenAI-compatible gateway:
74
+ ### Example: company-managed OpenAI-compatible gateway
36
75
 
37
76
  ```powershell
38
- $env:LLM_DEFAULT_HEADERS = '{"x-company-rbac":"your-rbac-token-here","Authorization":"sk-your-api-key-here"}'
39
- $env:LLM_BASE_URL = "https://llm-gateway.example.com"
77
+ $env:OPENAI_BASE_URL = "https://llm-gateway.example.com"
78
+ $env:OPENAI_DEFAULT_HEADERS = '{"x-company-rbac":"your-rbac-token-here","Authorization":"Bearer sk-your-api-key-here"}'
79
+ # LLM_PROVIDER is auto-detected as "openai-compatible" because LLM_BASE_URL/OPENAI_BASE_URL is set.
80
+ ```
81
+
82
+ ### Example: Google Gemini
83
+
84
+ ```powershell
85
+ $env:GOOGLE_GENERATIVE_AI_API_KEY = "..."
86
+ $env:LLM_MODEL = "gemini-2.0-flash"
40
87
  ```
41
88
 
42
89
  ## Usage
@@ -55,9 +102,10 @@ const markdown = await summarizeGitDiff({
55
102
  commitMessageExcludeRegexes: ['^\\[bot\\]'],
56
103
  commitMessageIncludeRegexes: ['^feat:'], // optional; OR across patterns
57
104
  teamName: 'Platform',
58
- systemPrompt: undefined, // optional; overrides DEFAULT_GIT_DIFF_SYSTEM_PROMPT
59
- model: 'gpt-4o-mini', // optional
60
- maxDiffChars: 120_000, // optional; also see LLM_MAX_DIFF_CHARS
105
+ systemPrompt: undefined, // optional; overrides DEFAULT_GIT_DIFF_SYSTEM_PROMPT
106
+ provider: 'anthropic', // optional; overrides LLM_PROVIDER env + auto-detection
107
+ model: 'claude-3-5-sonnet-latest', // optional
108
+ maxDiffChars: 120_000, // optional; also see LLM_MAX_DIFF_CHARS
61
109
  });
62
110
  ```
63
111
 
@@ -71,9 +119,27 @@ const markdown = await summarizeGitDiff({
71
119
  | `commitMessageExcludeRegexes` | Drop commits whose message matches **any** of these patterns. |
72
120
  | `teamName` | Adds a `Team:` line to the user payload for the model. |
73
121
  | `systemPrompt` | Replaces the default system prompt. |
74
- | `model` | Chat model id (default `gpt-4o-mini`). |
122
+ | `provider` | `LlmProviderId` wins over `LLM_PROVIDER` env and auto-detection. |
123
+ | `model` | Chat model id; overrides `LLM_MODEL` and the provider default. |
75
124
  | `maxDiffChars` | Caps unified diff size for the request. |
76
- | `openAiClientProvider` | `() => Promise<OpenAiLikeClient>` — bypasses env-based client creation (required in tests or when you wire the SDK yourself). |
125
+ | `llmModelProvider` | `() => Promise<LanguageModel>` — bypass env-based resolution entirely; hand-wire a Vercel AI SDK `LanguageModel` (required in tests or custom setups). |
126
+
127
+ ### Injecting your own `LanguageModel`
128
+
129
+ If you want full control — for example, to configure retries, middlewares, or hit an in-process mock — pass `llmModelProvider`:
130
+
131
+ ```ts
132
+ import { summarizeGitDiff } from '@mcarvin/smart-diff';
133
+ import { createAnthropic } from '@ai-sdk/anthropic';
134
+
135
+ const md = await summarizeGitDiff({
136
+ from: 'origin/main',
137
+ llmModelProvider: async () =>
138
+ createAnthropic({ apiKey: process.env.MY_ANTHROPIC_KEY })(
139
+ 'claude-3-5-sonnet-latest',
140
+ ),
141
+ });
142
+ ```
77
143
 
78
144
  ### Diff shape: single range vs per-commit
79
145
 
@@ -82,13 +148,28 @@ const markdown = await summarizeGitDiff({
82
148
 
83
149
  ### Lower-level API
84
150
 
85
- The package also exports helpers such as `createGitClient`, `getCommits`, `getDiff`, `getDiffSummary`, `getChangedFiles`, `filterCommitsByMessageRegexes`, `buildDiffPathspecs`, `generateSummary`, and OpenAI config utilities (`resolveLlmBaseUrl`, `shouldUseLlmGateway`, `createOpenAiLikeClient`, …). Use these if you build a custom pipeline but still want the same git and LLM behavior.
151
+ The package also exports helpers for building a custom pipeline on top of the same git and LLM behavior:
152
+
153
+ - **Git**: `createGitClient`, `getRepoRoot`, `getCommits`, `getDiff`, `getDiffSummary`, `getChangedFiles`, `filterCommitsByMessageRegexes`, `buildDiffPathspecs`
154
+ - **AI**: `generateSummary`, `resolveLlmMaxDiffChars`, `truncateUnifiedDiffForLlm`
155
+ - **Provider resolution**: `resolveLanguageModel`, `detectLlmProvider`, `isLlmProviderConfigured`, `defaultModelForProvider`, `resolveLlmBaseUrl`, `parseLlmDefaultHeadersFromEnv`
156
+ - **Constants / types**: `DEFAULT_GIT_DIFF_SYSTEM_PROMPT`, `LLM_GATEWAY_REQUIRED_MESSAGE`, `LlmProviderId`, `LlmModelProvider`, `ResolveLanguageModelOptions`, `GenerateSummaryInput`, `SummarizeFlags`
157
+
158
+ ## Migrating from 1.x → 2.x
159
+
160
+ v2 replaces the direct `openai` SDK dependency with the Vercel AI SDK. If you only rely on env-var configuration, your setup keeps working — `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_DEFAULT_HEADERS`, `LLM_*` equivalents, `OPENAI_MAX_DIFF_CHARS`, and `OPENAI_MAX_TOKENS` are all still honored.
161
+
162
+ Breaking changes:
163
+
164
+ - **Removed `openAiClientProvider` option** on `summarizeGitDiff`/`generateSummary`. Use `llmModelProvider: () => Promise<LanguageModel>` returning a Vercel AI SDK model instead.
165
+ - **Removed `OpenAiLikeClient` and `createOpenAiLikeClient` exports**, along with `shouldUseLlmGateway`. Use `isLlmProviderConfigured()` / `resolveLanguageModel()` instead.
166
+ - **`openai` npm package is no longer a dependency.** Remove it from your own `package.json` if you only depended on it transitively via smart-diff.
86
167
 
87
168
  ## Used By
88
169
 
89
170
  This package is used by:
90
171
 
91
- - [sf-git-ai-meta-insights](https://github.com/mcarvin8/sf-git-ai-meta-insights) = Salesforce metadata wrapper compatible with Salesforce DX projects
172
+ - [sf-git-ai-meta-insights](https://github.com/mcarvin8/sf-git-ai-meta-insights) Salesforce metadata wrapper compatible with Salesforce DX projects
92
173
 
93
174
  ## License
94
175
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var ai = require('ai');
3
4
  var node_path = require('node:path');
4
5
  var simpleGit = require('simple-git');
5
6
 
@@ -35,9 +36,55 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
35
36
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
36
37
  };
37
38
 
39
+ const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
40
+ const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
41
+ You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
42
+ Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
43
+ Produce a concise, developer-focused summary in Markdown.
44
+ Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
45
+ Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
46
+ If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
47
+ const LLM_GATEWAY_REQUIRED_MESSAGE = "No LLM provider configured. Set LLM_PROVIDER (openai | openai-compatible | anthropic | google | bedrock | mistral | cohere | groq | xai | deepseek), " +
48
+ "or a provider API key (OPENAI_API_KEY, LLM_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, MISTRAL_API_KEY, COHERE_API_KEY, GROQ_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY), " +
49
+ "or LLM_BASE_URL / OPENAI_BASE_URL for an OpenAI-compatible gateway, " +
50
+ "or JSON in OPENAI_DEFAULT_HEADERS / LLM_DEFAULT_HEADERS. " +
51
+ "Alternatively pass llmModelProvider or openAiClientProvider to generateSummary or summarizeGitDiff.";
52
+
53
+ const DEFAULT_MODEL_BY_PROVIDER = {
54
+ openai: "gpt-4o-mini",
55
+ "openai-compatible": "gpt-4o-mini",
56
+ anthropic: "claude-3-5-haiku-latest",
57
+ google: "gemini-2.0-flash",
58
+ bedrock: "anthropic.claude-3-5-haiku-20241022-v1:0",
59
+ mistral: "mistral-small-latest",
60
+ cohere: "command-r-08-2024",
61
+ groq: "llama-3.1-8b-instant",
62
+ xai: "grok-2-latest",
63
+ deepseek: "deepseek-chat",
64
+ };
65
+ const VALID_PROVIDERS = new Set([
66
+ "openai",
67
+ "openai-compatible",
68
+ "anthropic",
69
+ "google",
70
+ "bedrock",
71
+ "mistral",
72
+ "cohere",
73
+ "groq",
74
+ "xai",
75
+ "deepseek",
76
+ ]);
77
+ function readEnv(name) {
78
+ var _a;
79
+ const value = (_a = process.env[name]) === null || _a === void 0 ? void 0 : _a.trim();
80
+ return value && value.length > 0 ? value : undefined;
81
+ }
82
+ function isValidProviderId(value) {
83
+ return VALID_PROVIDERS.has(value);
84
+ }
38
85
  function resolveLlmBaseUrl() {
39
- var _a, _b, _c;
40
- return ((_b = (_a = process.env.LLM_BASE_URL) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_BASE_URL) === null || _c === void 0 ? void 0 : _c.trim());
86
+ var _a;
87
+ return (_a = readEnv("LLM_BASE_URL")) !== null && _a !== void 0 ? _a : readEnv("OPENAI_BASE_URL");
41
88
  }
42
89
  function parseHeaderJsonObject(raw) {
43
90
  const trimmed = raw === null || raw === void 0 ? void 0 : raw.trim();
@@ -68,87 +115,187 @@ function parseLlmDefaultHeadersFromEnv() {
68
115
  const merged = Object.assign(Object.assign({}, base), override);
69
116
  return Object.keys(merged).length > 0 ? merged : undefined;
70
117
  }
71
- function findAuthorizationHeaderName(headers) {
72
- return Object.keys(headers).find((k) => k.toLowerCase() === "authorization");
73
- }
74
- function stripBearerPrefix(value) {
118
+ function resolveOpenAiApiKey() {
75
119
  var _a;
76
- const trimmed = value.trim();
77
- const match = /^Bearer\s+(\S+)/i.exec(trimmed);
78
- return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : trimmed;
79
- }
80
- function splitPromotableAuthorizationFromHeaders(headers) {
81
- const authName = findAuthorizationHeaderName(headers);
82
- if (!authName) {
83
- return { defaultHeaders: headers };
84
- }
85
- const raw = headers[authName];
86
- if (!raw) {
87
- return { defaultHeaders: headers };
88
- }
89
- const token = stripBearerPrefix(raw);
90
- const looksBearer = /^Bearer\s+\S+/i.test(raw.trim());
91
- const looksOpenAiKey = /^sk-/i.test(token);
92
- if (!looksBearer && !looksOpenAiKey) {
93
- return { defaultHeaders: headers };
94
- }
95
- const next = Object.assign({}, headers);
96
- delete next[authName];
97
- return { defaultHeaders: next, apiKeyFromAuthHeader: token };
98
- }
99
- function shouldUseLlmGateway() {
100
- var _a, _b, _c;
101
- const apiKey = (_b = (_a = process.env.LLM_API_KEY) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_API_KEY) === null || _c === void 0 ? void 0 : _c.trim();
102
- if (apiKey)
103
- return true;
104
- if (resolveLlmBaseUrl())
105
- return true;
106
- const jsonHeaders = parseLlmDefaultHeadersFromEnv();
107
- if (jsonHeaders && Object.keys(jsonHeaders).length > 0)
108
- return true;
109
- return false;
110
- }
111
- function resolveOpenAiLikeClientInit() {
112
- var _a, _b, _c, _d, _e;
113
- const baseURL = resolveLlmBaseUrl();
114
- const mergedHeaders = (_a = parseLlmDefaultHeadersFromEnv()) !== null && _a !== void 0 ? _a : {};
115
- const envApiKey = (_e = (_c = (_b = process.env.LLM_API_KEY) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : (_d = process.env.OPENAI_API_KEY) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : "";
116
- let defaultHeaders;
117
- let apiKey = envApiKey;
118
- if (apiKey.length === 0) {
119
- const split = splitPromotableAuthorizationFromHeaders(mergedHeaders);
120
- if (split.apiKeyFromAuthHeader) {
121
- apiKey = split.apiKeyFromAuthHeader;
120
+ return (_a = readEnv("LLM_API_KEY")) !== null && _a !== void 0 ? _a : readEnv("OPENAI_API_KEY");
121
+ }
122
+ function detectLlmProvider() {
123
+ var _a, _b;
124
+ const explicit = (_a = readEnv("LLM_PROVIDER")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
125
+ if (explicit && isValidProviderId(explicit)) {
126
+ return explicit;
127
+ }
128
+ if (resolveLlmBaseUrl()) {
129
+ return "openai-compatible";
130
+ }
131
+ if (resolveOpenAiApiKey()) {
132
+ return "openai";
133
+ }
134
+ if (readEnv("ANTHROPIC_API_KEY"))
135
+ return "anthropic";
136
+ if ((_b = readEnv("GOOGLE_GENERATIVE_AI_API_KEY")) !== null && _b !== void 0 ? _b : readEnv("GOOGLE_API_KEY"))
137
+ return "google";
138
+ if (readEnv("MISTRAL_API_KEY"))
139
+ return "mistral";
140
+ if (readEnv("COHERE_API_KEY"))
141
+ return "cohere";
142
+ if (readEnv("GROQ_API_KEY"))
143
+ return "groq";
144
+ if (readEnv("XAI_API_KEY"))
145
+ return "xai";
146
+ if (readEnv("DEEPSEEK_API_KEY"))
147
+ return "deepseek";
148
+ if (parseLlmDefaultHeadersFromEnv())
149
+ return "openai";
150
+ return undefined;
151
+ }
152
+ function isLlmProviderConfigured() {
153
+ return detectLlmProvider() !== undefined;
154
+ }
155
+ function defaultModelForProvider(provider) {
156
+ return DEFAULT_MODEL_BY_PROVIDER[provider];
157
+ }
158
+ function createOpenAiModel(modelId) {
159
+ return __awaiter(this, void 0, void 0, function* () {
160
+ const { createOpenAI } = yield import('@ai-sdk/openai');
161
+ const apiKey = resolveOpenAiApiKey();
162
+ const headers = parseLlmDefaultHeadersFromEnv();
163
+ const provider = createOpenAI(Object.assign(Object.assign({}, (apiKey ? { apiKey } : {})), (headers ? { headers } : {})));
164
+ return provider(modelId);
165
+ });
166
+ }
167
+ function createOpenAiCompatibleModel(modelId) {
168
+ return __awaiter(this, void 0, void 0, function* () {
169
+ var _a;
170
+ const { createOpenAICompatible } = yield import('@ai-sdk/openai-compatible');
171
+ const baseURL = resolveLlmBaseUrl();
172
+ if (!baseURL) {
173
+ throw new Error("openai-compatible provider requires LLM_BASE_URL or OPENAI_BASE_URL to be set.");
122
174
  }
123
- defaultHeaders =
124
- Object.keys(split.defaultHeaders).length > 0
125
- ? split.defaultHeaders
126
- : undefined;
127
- }
128
- else {
129
- defaultHeaders =
130
- Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
131
- }
132
- return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : "unused" }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
175
+ const apiKey = resolveOpenAiApiKey();
176
+ const headers = parseLlmDefaultHeadersFromEnv();
177
+ const provider = createOpenAICompatible(Object.assign(Object.assign({ name: (_a = readEnv("LLM_PROVIDER_NAME")) !== null && _a !== void 0 ? _a : "openai-compatible", baseURL }, (apiKey ? { apiKey } : {})), (headers ? { headers } : {})));
178
+ return provider(modelId);
179
+ });
180
+ }
181
+ function wrapMissingPeer(failure) {
182
+ const err = new Error(`Failed to load optional provider package "${failure.pkg}" for LLM_PROVIDER="${failure.provider}". ` +
183
+ `Install it with \`npm install ${failure.pkg}\`.`);
184
+ err.cause = failure.cause;
185
+ return err;
133
186
  }
134
- function createOpenAiLikeClient() {
187
+ function importOptional(provider, pkg, loader) {
135
188
  return __awaiter(this, void 0, void 0, function* () {
136
- const { default: OpenAI } = yield import('openai');
137
- return new OpenAI(resolveOpenAiLikeClientInit());
189
+ try {
190
+ return yield loader();
191
+ }
192
+ catch (cause) {
193
+ throw wrapMissingPeer({ provider, pkg, cause });
194
+ }
195
+ });
196
+ }
197
+ function createAnthropicModel(modelId) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ const mod = yield importOptional("anthropic", "@ai-sdk/anthropic", () => import('@ai-sdk/anthropic'));
200
+ const apiKey = readEnv("ANTHROPIC_API_KEY");
201
+ const provider = mod.createAnthropic(apiKey ? { apiKey } : undefined);
202
+ return provider(modelId);
203
+ });
204
+ }
205
+ function createGoogleModel(modelId) {
206
+ return __awaiter(this, void 0, void 0, function* () {
207
+ var _a;
208
+ const mod = yield importOptional("google", "@ai-sdk/google", () => import('@ai-sdk/google'));
209
+ const apiKey = (_a = readEnv("GOOGLE_GENERATIVE_AI_API_KEY")) !== null && _a !== void 0 ? _a : readEnv("GOOGLE_API_KEY");
210
+ const provider = mod.createGoogleGenerativeAI(apiKey ? { apiKey } : undefined);
211
+ return provider(modelId);
212
+ });
213
+ }
214
+ function createBedrockModel(modelId) {
215
+ return __awaiter(this, void 0, void 0, function* () {
216
+ const mod = yield importOptional("bedrock", "@ai-sdk/amazon-bedrock", () => import('@ai-sdk/amazon-bedrock'));
217
+ const provider = mod.createAmazonBedrock();
218
+ return provider(modelId);
219
+ });
220
+ }
221
+ function createMistralModel(modelId) {
222
+ return __awaiter(this, void 0, void 0, function* () {
223
+ const mod = yield importOptional("mistral", "@ai-sdk/mistral", () => import('@ai-sdk/mistral'));
224
+ const apiKey = readEnv("MISTRAL_API_KEY");
225
+ const provider = mod.createMistral(apiKey ? { apiKey } : undefined);
226
+ return provider(modelId);
227
+ });
228
+ }
229
+ function createCohereModel(modelId) {
230
+ return __awaiter(this, void 0, void 0, function* () {
231
+ const mod = yield importOptional("cohere", "@ai-sdk/cohere", () => import('@ai-sdk/cohere'));
232
+ const apiKey = readEnv("COHERE_API_KEY");
233
+ const provider = mod.createCohere(apiKey ? { apiKey } : undefined);
234
+ return provider(modelId);
235
+ });
236
+ }
237
+ function createGroqModel(modelId) {
238
+ return __awaiter(this, void 0, void 0, function* () {
239
+ const mod = yield importOptional("groq", "@ai-sdk/groq", () => import('@ai-sdk/groq'));
240
+ const apiKey = readEnv("GROQ_API_KEY");
241
+ const provider = mod.createGroq(apiKey ? { apiKey } : undefined);
242
+ return provider(modelId);
243
+ });
244
+ }
245
+ function createXaiModel(modelId) {
246
+ return __awaiter(this, void 0, void 0, function* () {
247
+ const mod = yield importOptional("xai", "@ai-sdk/xai", () => import('@ai-sdk/xai'));
248
+ const apiKey = readEnv("XAI_API_KEY");
249
+ const provider = mod.createXai(apiKey ? { apiKey } : undefined);
250
+ return provider(modelId);
251
+ });
252
+ }
253
+ function createDeepseekModel(modelId) {
254
+ return __awaiter(this, void 0, void 0, function* () {
255
+ const mod = yield importOptional("deepseek", "@ai-sdk/deepseek", () => import('@ai-sdk/deepseek'));
256
+ const apiKey = readEnv("DEEPSEEK_API_KEY");
257
+ const provider = mod.createDeepSeek(apiKey ? { apiKey } : undefined);
258
+ return provider(modelId);
259
+ });
260
+ }
261
+ function resolveLanguageModel() {
262
+ return __awaiter(this, arguments, void 0, function* (options = {}) {
263
+ var _a, _b, _c;
264
+ const provider = (_a = options.provider) !== null && _a !== void 0 ? _a : detectLlmProvider();
265
+ if (!provider) {
266
+ throw new Error("No LLM provider could be resolved. Set LLM_PROVIDER or a provider API key " +
267
+ "(OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, MISTRAL_API_KEY, " +
268
+ "COHERE_API_KEY, GROQ_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY), or LLM_BASE_URL for an OpenAI-compatible gateway.");
269
+ }
270
+ const modelId = (_c = (_b = options.model) !== null && _b !== void 0 ? _b : readEnv("LLM_MODEL")) !== null && _c !== void 0 ? _c : defaultModelForProvider(provider);
271
+ switch (provider) {
272
+ case "openai":
273
+ return createOpenAiModel(modelId);
274
+ case "openai-compatible":
275
+ return createOpenAiCompatibleModel(modelId);
276
+ case "anthropic":
277
+ return createAnthropicModel(modelId);
278
+ case "google":
279
+ return createGoogleModel(modelId);
280
+ case "bedrock":
281
+ return createBedrockModel(modelId);
282
+ case "mistral":
283
+ return createMistralModel(modelId);
284
+ case "cohere":
285
+ return createCohereModel(modelId);
286
+ case "groq":
287
+ return createGroqModel(modelId);
288
+ case "xai":
289
+ return createXaiModel(modelId);
290
+ case "deepseek":
291
+ return createDeepseekModel(modelId);
292
+ default: {
293
+ const _exhaustive = provider;
294
+ throw new Error(`Unhandled LLM provider: ${String(_exhaustive)}`);
295
+ }
296
+ }
138
297
  });
139
298
  }
140
-
141
- const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
142
- const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
143
- You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
144
- Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
145
- Produce a concise, developer-focused summary in Markdown.
146
- Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
147
- Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
148
- If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
149
- const LLM_GATEWAY_REQUIRED_MESSAGE = "No LLM gateway configured. Set OPENAI_API_KEY or LLM_API_KEY, and/or LLM_BASE_URL or OPENAI_BASE_URL, " +
150
- "and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. " +
151
- "Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.";
152
299
 
153
300
  function resolveLlmMaxDiffChars(cliOverride) {
154
301
  var _a;
@@ -173,17 +320,33 @@ function truncateUnifiedDiffForLlm(diffText, maxChars) {
173
320
  const marker = `\n\n--- TRUNCATED: unified diff was ${diffText.length} characters; only the first ${maxChars} were sent. Narrow the ref range, adjust commit/path filters, or raise maxDiffChars / LLM_MAX_DIFF_CHARS only if your model context allows. ---\n`;
174
321
  return diffText.slice(0, maxChars) + marker;
175
322
  }
323
+ function markdownDiffTruncationNotice(originalChars, maxChars) {
324
+ return `> **Truncated diff:** The unified diff was ${originalChars} characters; only the first ${maxChars} were sent to the model. The summary may not reflect the full change set. Narrow the ref range, adjust path filters, or raise \`maxDiffChars\` / \`LLM_MAX_DIFF_CHARS\`—often together with switching to a model whose context window can fit a larger prompt.\n\n`;
325
+ }
326
+ function resolveMaxOutputTokens() {
327
+ var _a;
328
+ const raw = (_a = process.env.LLM_MAX_TOKENS) !== null && _a !== void 0 ? _a : process.env.OPENAI_MAX_TOKENS;
329
+ const parsed = raw !== undefined ? Number.parseInt(raw, 10) : 4000;
330
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
331
+ }
176
332
  function generateSummary(input) {
177
333
  return __awaiter(this, void 0, void 0, function* () {
178
- var _a, _b;
179
- const { diffText, fileNames, commits, flags, openAiClientProvider, diffSummary, } = input;
180
- if (!shouldUseLlmGateway() && openAiClientProvider === undefined) {
334
+ var _a;
335
+ const { diffText, fileNames, commits, flags, llmModelProvider, diffSummary, } = input;
336
+ if (!llmModelProvider && !isLlmProviderConfigured()) {
181
337
  throw new Error(LLM_GATEWAY_REQUIRED_MESSAGE);
182
338
  }
183
339
  const maxDiffChars = resolveLlmMaxDiffChars(flags.maxDiffChars);
340
+ const diffTruncated = diffText.length > maxDiffChars;
184
341
  const diffForLlm = truncateUnifiedDiffForLlm(diffText, maxDiffChars);
185
- const userContent = buildOpenAiUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
186
- return callOpenAi(userContent, (_a = flags.model) !== null && _a !== void 0 ? _a : "gpt-4o-mini", (_b = flags.systemPrompt) !== null && _b !== void 0 ? _b : DEFAULT_GIT_DIFF_SYSTEM_PROMPT, openAiClientProvider !== null && openAiClientProvider !== void 0 ? openAiClientProvider : (() => __awaiter(this, void 0, void 0, function* () { return createOpenAiLikeClient(); })));
342
+ const userContent = buildUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
343
+ const systemPrompt = (_a = flags.systemPrompt) !== null && _a !== void 0 ? _a : DEFAULT_GIT_DIFF_SYSTEM_PROMPT;
344
+ const maxOutputTokens = resolveMaxOutputTokens();
345
+ const summary = yield callLlm(userContent, systemPrompt, maxOutputTokens, llmModelProvider, flags);
346
+ if (!diffTruncated) {
347
+ return summary;
348
+ }
349
+ return markdownDiffTruncationNotice(diffText.length, maxDiffChars) + summary;
187
350
  });
188
351
  }
189
352
  function formatRegexFilterLines(flags) {
@@ -206,7 +369,7 @@ function formatRegexFilterLines(flags) {
206
369
  return (`${incLine}${excLine}` +
207
370
  "Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n");
208
371
  }
209
- function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary) {
372
+ function buildUserContent(flags, commits, fileNames, diffText, diffSummary) {
210
373
  var _a, _b;
211
374
  const from = flags.from;
212
375
  const to = (_a = flags.to) !== null && _a !== void 0 ? _a : "HEAD";
@@ -236,31 +399,21 @@ function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary
236
399
  "=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n" +
237
400
  diffText);
238
401
  }
239
- function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
402
+ function callLlm(userContent, systemPrompt, maxOutputTokens, llmModelProvider, flags) {
240
403
  return __awaiter(this, void 0, void 0, function* () {
241
- var _a, _b, _c, _d, _e, _f;
242
- const client = yield openAiClientProvider();
243
- const maxTokensRaw = (_a = process.env.LLM_MAX_TOKENS) !== null && _a !== void 0 ? _a : process.env.OPENAI_MAX_TOKENS;
244
- const parsed = maxTokensRaw !== undefined ? Number.parseInt(maxTokensRaw, 10) : 4000;
245
- const maxTokens = Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
246
- const response = yield client.chat.completions.create({
404
+ var _a, _b;
405
+ const model = llmModelProvider
406
+ ? yield llmModelProvider()
407
+ : yield resolveLanguageModel(Object.assign(Object.assign({}, (flags.provider ? { provider: flags.provider } : {})), (flags.model ? { model: flags.model } : {})));
408
+ const result = yield ai.generateText({
247
409
  model,
248
- messages: [
249
- {
250
- role: "system",
251
- content: systemPrompt,
252
- },
253
- {
254
- role: "user",
255
- content: userContent,
256
- },
257
- ],
410
+ system: systemPrompt,
411
+ prompt: userContent,
258
412
  temperature: 0.2,
259
- max_tokens: maxTokens,
413
+ maxOutputTokens,
260
414
  });
261
- const typedResponse = response;
262
- const text = (_f = (_e = (_d = (_c = (_b = typedResponse.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : "";
263
- return text.length > 0 ? text : "No summary generated by OpenAI.";
415
+ const text = (_b = (_a = result.text) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : "";
416
+ return text.length > 0 ? text : "No summary generated by the model.";
264
417
  });
265
418
  }
266
419
 
@@ -704,6 +857,7 @@ function summarizeGitDiff(options) {
704
857
  to,
705
858
  team: options.teamName,
706
859
  model: options.model,
860
+ provider: options.provider,
707
861
  maxDiffChars: options.maxDiffChars,
708
862
  systemPrompt: options.systemPrompt,
709
863
  commitMessageIncludeRegexes: options.commitMessageIncludeRegexes,
@@ -714,7 +868,7 @@ function summarizeGitDiff(options) {
714
868
  fileNames,
715
869
  commits: filteredCommits,
716
870
  flags: summarizeFlags,
717
- openAiClientProvider: options.openAiClientProvider,
871
+ llmModelProvider: options.llmModelProvider,
718
872
  diffSummary,
719
873
  });
720
874
  });
@@ -724,7 +878,8 @@ exports.DEFAULT_GIT_DIFF_SYSTEM_PROMPT = DEFAULT_GIT_DIFF_SYSTEM_PROMPT;
724
878
  exports.LLM_GATEWAY_REQUIRED_MESSAGE = LLM_GATEWAY_REQUIRED_MESSAGE;
725
879
  exports.buildDiffPathspecs = buildDiffPathspecs;
726
880
  exports.createGitClient = createGitClient;
727
- exports.createOpenAiLikeClient = createOpenAiLikeClient;
881
+ exports.defaultModelForProvider = defaultModelForProvider;
882
+ exports.detectLlmProvider = detectLlmProvider;
728
883
  exports.filterCommitsByMessageRegexes = filterCommitsByMessageRegexes;
729
884
  exports.generateSummary = generateSummary;
730
885
  exports.getChangedFiles = getChangedFiles;
@@ -732,12 +887,11 @@ exports.getCommits = getCommits;
732
887
  exports.getDiff = getDiff;
733
888
  exports.getDiffSummary = getDiffSummary;
734
889
  exports.getRepoRoot = getRepoRoot;
890
+ exports.isLlmProviderConfigured = isLlmProviderConfigured;
735
891
  exports.parseLlmDefaultHeadersFromEnv = parseLlmDefaultHeadersFromEnv;
892
+ exports.resolveLanguageModel = resolveLanguageModel;
736
893
  exports.resolveLlmBaseUrl = resolveLlmBaseUrl;
737
894
  exports.resolveLlmMaxDiffChars = resolveLlmMaxDiffChars;
738
- exports.resolveOpenAiLikeClientInit = resolveOpenAiLikeClientInit;
739
- exports.shouldUseLlmGateway = shouldUseLlmGateway;
740
- exports.splitPromotableAuthorizationFromHeaders = splitPromotableAuthorizationFromHeaders;
741
895
  exports.summarizeGitDiff = summarizeGitDiff;
742
896
  exports.truncateUnifiedDiffForLlm = truncateUnifiedDiffForLlm;
743
897
  //# sourceMappingURL=index.cjs.map