patchling 0.2.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/src/env.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Environment access that works in both Node and the browser.
3
+ *
4
+ * In Node, reads from `process.env`. In the browser there is no `process`, so
5
+ * callers can register overrides (e.g. values obtained via OAuth sign-in)
6
+ * through `setEnv`, which are consulted first.
7
+ */
8
+
9
+ const overrides = Object.create(null);
10
+
11
+ /**
12
+ * Set an environment-style override (used in the browser where there is no
13
+ * `process.env`). Pass `undefined` to clear an override.
14
+ * @param {string} name
15
+ * @param {string | undefined} value
16
+ */
17
+ export function setEnv(name, value) {
18
+ if (value === undefined) {
19
+ delete overrides[name];
20
+ } else {
21
+ overrides[name] = value;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Read an environment variable. Overrides take precedence over `process.env`.
27
+ * @param {string} name
28
+ * @param {string} [fallback]
29
+ * @returns {string | undefined}
30
+ */
31
+ export function getEnv(name, fallback = undefined) {
32
+ if (name in overrides) return overrides[name];
33
+ if (typeof process !== 'undefined' && process.env && name in process.env) {
34
+ return process.env[name];
35
+ }
36
+ return fallback;
37
+ }
38
+
39
+ export const DEFAULT_MODEL = 'xiaomi/mimo-v2.5-pro-ultraspeed';
40
+ export const DEFAULT_BASE_URL = 'https://nano-gpt.com/api/v1/';
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Rebuild the environment string from a `{ path: content }` map.
3
+ * @param {Record<string, string>} filesDict
4
+ * @returns {string}
5
+ */
6
+ export function buildEnvironment(filesDict: Record<string, string>): string;
7
+ /**
8
+ * Build the prompt, call the LLM, and extract the unified diff from the
9
+ * ```diff fenced blocks of the response.
10
+ *
11
+ * @param {string} systemPrompt
12
+ * @param {string} userPrompt
13
+ * @param {string} filesContent
14
+ * @param {string} model
15
+ * @param {object} [opts]
16
+ * @param {number} [opts.temperature]
17
+ * @param {number} [opts.maxTokens]
18
+ * @param {string} [opts.apiKey]
19
+ * @param {string} [opts.baseUrl]
20
+ * @param {number} [opts.budgetTokens]
21
+ * @param {Array} [opts.images]
22
+ * @param {(text: string) => void} [opts.onToken] stream callback, called per content delta
23
+ * @param {Function} [opts.callLlm]
24
+ * @returns {Promise<{ fullResponse: string, diff: string, promptTokens: number,
25
+ * completionTokens: number, totalTokens: number }>}
26
+ */
27
+ export function callLlmForDiff(systemPrompt: string, userPrompt: string, filesContent: string, model: string, opts?: {
28
+ temperature?: number;
29
+ maxTokens?: number;
30
+ apiKey?: string;
31
+ baseUrl?: string;
32
+ budgetTokens?: number;
33
+ images?: any[];
34
+ onToken?: (text: string) => void;
35
+ callLlm?: Function;
36
+ }): Promise<{
37
+ fullResponse: string;
38
+ diff: string;
39
+ promptTokens: number;
40
+ completionTokens: number;
41
+ totalTokens: number;
42
+ }>;
43
+ /**
44
+ * Generate a git diff from an environment string and a goal.
45
+ *
46
+ * @param {string} environment the codebase as a string (see buildEnvironment)
47
+ * @param {string} goal natural-language instruction
48
+ * @param {object} [opts]
49
+ * @param {string} [opts.model]
50
+ * @param {number} [opts.temperature]
51
+ * @param {number} [opts.maxTokens]
52
+ * @param {string} [opts.apiKey]
53
+ * @param {string} [opts.baseUrl]
54
+ * @param {string} [opts.prepend] text prepended to the system prompt
55
+ * @param {number} [opts.budgetTokens]
56
+ * @param {Array} [opts.images]
57
+ * @param {(text: string) => void} [opts.onToken] stream callback — when
58
+ * provided, the LLM response is streamed and this is called once per token
59
+ * (content delta) as it arrives. Tokens are the raw model output (which may
60
+ * include prose around the ```diff block); the resolved value is still the
61
+ * extracted diff.
62
+ * @param {Function} [opts.callLlm] injectable LLM client (for testing)
63
+ * @param {(usage: { promptTokens: number, completionTokens: number,
64
+ * totalTokens: number }) => void} [opts.onUsage] optional callback invoked
65
+ * with token usage after the LLM call completes, so callers can surface
66
+ * cost/usage without changing the return type of this function.
67
+ * @returns {Promise<string>} the unified diff text
68
+ */
69
+ export function generateDiff(environment: string, goal: string, opts?: {
70
+ model?: string;
71
+ temperature?: number;
72
+ maxTokens?: number;
73
+ apiKey?: string;
74
+ baseUrl?: string;
75
+ prepend?: string;
76
+ budgetTokens?: number;
77
+ images?: any[];
78
+ onToken?: (text: string) => void;
79
+ callLlm?: Function;
80
+ onUsage?: (usage: {
81
+ promptTokens: number;
82
+ completionTokens: number;
83
+ totalTokens: number;
84
+ }) => void;
85
+ }): Promise<string>;
86
+ /**
87
+ * AI-powered application of a single-file diff. Returns the full LLM response
88
+ * (before think/reasoning stripping).
89
+ *
90
+ * @param {string} filePath
91
+ * @param {string} originalContent
92
+ * @param {string} fileDiff
93
+ * @param {string} model
94
+ * @param {object} [opts]
95
+ * @param {string} [opts.apiKey]
96
+ * @param {string} [opts.baseUrl]
97
+ * @param {string} [opts.extraPrompt] appended to the user prompt
98
+ * @param {number} [opts.maxTokens]
99
+ * @param {(text: string) => void} [opts.onToken] stream callback, called once
100
+ * per token of the rewritten file as it arrives. Note the streamed text is
101
+ * the raw model output — `<think>` blocks are only stripped from the final
102
+ * result by {@link callLlmForApplyWithThink}.
103
+ * @param {Function} [opts.callLlm] injectable LLM client (for testing)
104
+ * @returns {Promise<string>}
105
+ */
106
+ export function callLlmForApply(filePath: string, originalContent: string, fileDiff: string, model: string, opts?: {
107
+ apiKey?: string;
108
+ baseUrl?: string;
109
+ extraPrompt?: string;
110
+ maxTokens?: number;
111
+ onToken?: (text: string) => void;
112
+ callLlm?: Function;
113
+ }): Promise<string>;
114
+ /**
115
+ * Apply a diff to a single file's content, stripping `<think>` blocks and
116
+ * reasoning preambles from the LLM response.
117
+ *
118
+ * @param {string} filePath
119
+ * @param {string} originalContent
120
+ * @param {string} fileDiff
121
+ * @param {string} model
122
+ * @param {object} [opts]
123
+ * @returns {Promise<string>} the cleaned file content
124
+ */
125
+ export function callLlmForApplyWithThink(filePath: string, originalContent: string, fileDiff: string, model: string, opts?: object): Promise<string>;
126
+ /**
127
+ * Apply unified diffs to a map of file contents. Handles creations,
128
+ * modifications, and deletions. Returns a new map (the input is not mutated).
129
+ * Deleted files are omitted.
130
+ *
131
+ * Each file is processed in parallel. By default, every file is first applied
132
+ * deterministically (no LLM call) via {@link applyPatchToFile}; only files
133
+ * whose patch does not apply cleanly fall back to AI-powered conflict
134
+ * resolution. Set `opts.forceLlm` to skip the deterministic fast path and send
135
+ * every file to the LLM.
136
+ *
137
+ * @param {string} diffText
138
+ * @param {Record<string, string>} files
139
+ * @param {object} [opts]
140
+ * @param {string} [opts.model]
141
+ * @param {string} [opts.apiKey]
142
+ * @param {string} [opts.baseUrl]
143
+ * @param {number} [opts.maxTokens]
144
+ * @param {boolean} [opts.forceLlm] skip the deterministic fast path
145
+ * @param {(text: string, path: string) => void} [opts.onToken] stream
146
+ * callback for LLM-resolved files. Files are processed concurrently, so
147
+ * token calls for different files interleave; the second argument is the
148
+ * file path the token belongs to, letting callers demultiplex. Files taken
149
+ * by the deterministic fast path emit no tokens (no LLM call is made).
150
+ * @param {Function} [opts.callLlmForApply] injectable single-file applier
151
+ * @returns {Promise<Record<string, string>>}
152
+ */
153
+ export function smartapply(diffText: string, files: Record<string, string>, opts?: {
154
+ model?: string;
155
+ apiKey?: string;
156
+ baseUrl?: string;
157
+ maxTokens?: number;
158
+ forceLlm?: boolean;
159
+ onToken?: (text: string, path: string) => void;
160
+ callLlmForApply?: Function;
161
+ }): Promise<Record<string, string>>;
package/src/gptdiff.js ADDED
@@ -0,0 +1,352 @@
1
+ /**
2
+ * gptdiff-js core: generateDiff + smartapply.
3
+ *
4
+ * A browser-first port of the gptdiff Python package, scoped to the two main
5
+ * APIs. LLM access is injectable (`opts.callLlm` / `opts.callLlmForApply`) so
6
+ * the logic is fully unit-testable without network access.
7
+ */
8
+
9
+ import { parseDiffPerFile, applyPatchToFile } from './applydiff.js';
10
+ import {
11
+ swallowReasoning,
12
+ stripBadOutput,
13
+ stripThinkTags,
14
+ extractDiffBlocks,
15
+ } from './text.js';
16
+ import {
17
+ callLlm as defaultCallLlm,
18
+ resolveApiKey,
19
+ resolveBaseUrl,
20
+ } from './llm.js';
21
+ import { getEnv, DEFAULT_MODEL } from './env.js';
22
+
23
+ const DIFF_TOOL_PROMPT = `Save the calculated diff as used in 'git apply'. Should include the file and line number. For example:
24
+ \`\`\`diff
25
+ a/file.py b/file.py
26
+ --- a/file.py
27
+ +++ b/file.py
28
+ @@ -1,2 +1,2 @@
29
+ -def old():
30
+ +def new():
31
+ \`\`\`
32
+
33
+ You must include the '--- file' and/or '+++ file' part of the diff. File modifications should include both.
34
+ `;
35
+
36
+ const APPLY_SYSTEM_PROMPT = `Please apply the diff to this file. Return the result in a block. Write the entire file.
37
+
38
+ 1. Carefully apply all changes from the diff
39
+ 2. Preserve surrounding context that isn't changed
40
+ 3. Only return the final file content, do not add any additional markup and do not add a code block
41
+ 4. You must return the entire file. It overwrites the existing file.`;
42
+
43
+ /**
44
+ * Rebuild the environment string from a `{ path: content }` map.
45
+ * @param {Record<string, string>} filesDict
46
+ * @returns {string}
47
+ */
48
+ export function buildEnvironment(filesDict) {
49
+ const env = [];
50
+ for (const [path, content] of Object.entries(filesDict)) {
51
+ env.push(`File: ${path}`);
52
+ env.push('Content:');
53
+ env.push(content);
54
+ }
55
+ return env.join('\n');
56
+ }
57
+
58
+ function resolveModel(model) {
59
+ return model || getEnv('GPTDIFF_MODEL', DEFAULT_MODEL) || DEFAULT_MODEL;
60
+ }
61
+
62
+ function usageFrom(response) {
63
+ const usage = response && response.usage;
64
+ if (!usage) return { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
65
+ const promptTokens = usage.prompt_tokens ?? usage.input_tokens ?? 0;
66
+ const completionTokens = usage.completion_tokens ?? usage.output_tokens ?? 0;
67
+ const totalTokens = usage.total_tokens ?? promptTokens + completionTokens;
68
+ return { promptTokens, completionTokens, totalTokens };
69
+ }
70
+
71
+ /**
72
+ * Build the prompt, call the LLM, and extract the unified diff from the
73
+ * ```diff fenced blocks of the response.
74
+ *
75
+ * @param {string} systemPrompt
76
+ * @param {string} userPrompt
77
+ * @param {string} filesContent
78
+ * @param {string} model
79
+ * @param {object} [opts]
80
+ * @param {number} [opts.temperature]
81
+ * @param {number} [opts.maxTokens]
82
+ * @param {string} [opts.apiKey]
83
+ * @param {string} [opts.baseUrl]
84
+ * @param {number} [opts.budgetTokens]
85
+ * @param {Array} [opts.images]
86
+ * @param {(text: string) => void} [opts.onToken] stream callback, called per content delta
87
+ * @param {Function} [opts.callLlm]
88
+ * @returns {Promise<{ fullResponse: string, diff: string, promptTokens: number,
89
+ * completionTokens: number, totalTokens: number }>}
90
+ */
91
+ export async function callLlmForDiff(systemPrompt, userPrompt, filesContent, model, opts = {}) {
92
+ const {
93
+ temperature = 1.0,
94
+ maxTokens = null,
95
+ apiKey,
96
+ baseUrl,
97
+ budgetTokens = null,
98
+ images = null,
99
+ onToken = null,
100
+ callLlm = defaultCallLlm,
101
+ } = opts;
102
+
103
+ const fullSystemPrompt = systemPrompt + '\n' + DIFF_TOOL_PROMPT;
104
+
105
+ let effectiveUserPrompt = userPrompt;
106
+ if (model && model.includes('gemini')) {
107
+ effectiveUserPrompt = fullSystemPrompt + '\n' + userPrompt;
108
+ }
109
+
110
+ const userText = effectiveUserPrompt + '\n' + filesContent;
111
+ /** @type {string | Array<{type: string, text?: string, image_url?: {url: string}}>} */
112
+ let userContent = userText;
113
+ if (images && images.length) {
114
+ /** @type {Array<{type: string, text?: string, image_url?: {url: string}}>} */
115
+ const blocks = [{ type: 'text', text: userText }];
116
+ for (const image of images) {
117
+ const dataUrl = `data:${image.media_type};base64,${image.data}`;
118
+ blocks.push({ type: 'image_url', image_url: { url: dataUrl } });
119
+ }
120
+ userContent = blocks;
121
+ }
122
+
123
+ const messages = [
124
+ { role: 'system', content: fullSystemPrompt },
125
+ { role: 'user', content: userContent },
126
+ ];
127
+
128
+ const response = await callLlm({
129
+ apiKey: resolveApiKey(apiKey),
130
+ baseUrl: resolveBaseUrl(baseUrl),
131
+ model,
132
+ messages,
133
+ maxTokens,
134
+ temperature,
135
+ budgetTokens,
136
+ onToken,
137
+ });
138
+
139
+ const { promptTokens, completionTokens, totalTokens } = usageFrom(response);
140
+
141
+ let fullResponse = (response.choices[0].message.content || '').trim();
142
+ const [stripped] = swallowReasoning(fullResponse);
143
+ fullResponse = stripped;
144
+
145
+ const diff = extractDiffBlocks(fullResponse).join('\n');
146
+
147
+ return { fullResponse, diff, promptTokens, completionTokens, totalTokens };
148
+ }
149
+
150
+ /**
151
+ * Generate a git diff from an environment string and a goal.
152
+ *
153
+ * @param {string} environment the codebase as a string (see buildEnvironment)
154
+ * @param {string} goal natural-language instruction
155
+ * @param {object} [opts]
156
+ * @param {string} [opts.model]
157
+ * @param {number} [opts.temperature]
158
+ * @param {number} [opts.maxTokens]
159
+ * @param {string} [opts.apiKey]
160
+ * @param {string} [opts.baseUrl]
161
+ * @param {string} [opts.prepend] text prepended to the system prompt
162
+ * @param {number} [opts.budgetTokens]
163
+ * @param {Array} [opts.images]
164
+ * @param {(text: string) => void} [opts.onToken] stream callback — when
165
+ * provided, the LLM response is streamed and this is called once per token
166
+ * (content delta) as it arrives. Tokens are the raw model output (which may
167
+ * include prose around the ```diff block); the resolved value is still the
168
+ * extracted diff.
169
+ * @param {Function} [opts.callLlm] injectable LLM client (for testing)
170
+ * @param {(usage: { promptTokens: number, completionTokens: number,
171
+ * totalTokens: number }) => void} [opts.onUsage] optional callback invoked
172
+ * with token usage after the LLM call completes, so callers can surface
173
+ * cost/usage without changing the return type of this function.
174
+ * @returns {Promise<string>} the unified diff text
175
+ */
176
+ export async function generateDiff(environment, goal, opts = {}) {
177
+ const model = resolveModel(opts.model);
178
+ const prepend = opts.prepend ? opts.prepend + '\n' : '';
179
+ const systemPrompt = prepend + 'Output a full unified git diff into a "```diff" block.';
180
+
181
+ const { diff, promptTokens, completionTokens, totalTokens } = await callLlmForDiff(
182
+ systemPrompt,
183
+ goal,
184
+ environment,
185
+ model,
186
+ {
187
+ temperature: opts.temperature ?? 1.0,
188
+ // No default cap: let the model emit its full diff (avoids silent truncation
189
+ // that produces an unparseable diff). Callers may still pass an explicit maxTokens.
190
+ maxTokens: opts.maxTokens ?? null,
191
+ apiKey: opts.apiKey,
192
+ baseUrl: opts.baseUrl,
193
+ budgetTokens: opts.budgetTokens ?? null,
194
+ images: opts.images ?? null,
195
+ onToken: opts.onToken ?? null,
196
+ callLlm: opts.callLlm,
197
+ },
198
+ );
199
+
200
+ if (typeof opts.onUsage === 'function') {
201
+ opts.onUsage({ promptTokens, completionTokens, totalTokens });
202
+ }
203
+
204
+ return diff;
205
+ }
206
+
207
+ /**
208
+ * AI-powered application of a single-file diff. Returns the full LLM response
209
+ * (before think/reasoning stripping).
210
+ *
211
+ * @param {string} filePath
212
+ * @param {string} originalContent
213
+ * @param {string} fileDiff
214
+ * @param {string} model
215
+ * @param {object} [opts]
216
+ * @param {string} [opts.apiKey]
217
+ * @param {string} [opts.baseUrl]
218
+ * @param {string} [opts.extraPrompt] appended to the user prompt
219
+ * @param {number} [opts.maxTokens]
220
+ * @param {(text: string) => void} [opts.onToken] stream callback, called once
221
+ * per token of the rewritten file as it arrives. Note the streamed text is
222
+ * the raw model output — `<think>` blocks are only stripped from the final
223
+ * result by {@link callLlmForApplyWithThink}.
224
+ * @param {Function} [opts.callLlm] injectable LLM client (for testing)
225
+ * @returns {Promise<string>}
226
+ */
227
+ export async function callLlmForApply(filePath, originalContent, fileDiff, model, opts = {}) {
228
+ const {
229
+ apiKey,
230
+ baseUrl,
231
+ extraPrompt = null,
232
+ maxTokens = null,
233
+ onToken = null,
234
+ callLlm = defaultCallLlm,
235
+ } = opts;
236
+
237
+ let userPrompt = `File: ${filePath}
238
+ File contents:
239
+ \`\`\`
240
+ ${originalContent}
241
+ \`\`\`
242
+
243
+ Diff to apply:
244
+ \`\`\`diff
245
+ ${fileDiff}
246
+ \`\`\``;
247
+ if (extraPrompt) userPrompt += `\n\n${extraPrompt}`;
248
+ if (model && model.includes('gemini')) {
249
+ userPrompt = APPLY_SYSTEM_PROMPT + '\n' + userPrompt;
250
+ }
251
+
252
+ const messages = [
253
+ { role: 'system', content: APPLY_SYSTEM_PROMPT },
254
+ { role: 'user', content: userPrompt },
255
+ ];
256
+
257
+ const response = await callLlm({
258
+ apiKey: resolveApiKey(apiKey),
259
+ baseUrl: resolveBaseUrl(baseUrl),
260
+ model,
261
+ messages,
262
+ maxTokens,
263
+ temperature: 0.0,
264
+ onToken,
265
+ });
266
+ return response.choices[0].message.content;
267
+ }
268
+
269
+ /**
270
+ * Apply a diff to a single file's content, stripping `<think>` blocks and
271
+ * reasoning preambles from the LLM response.
272
+ *
273
+ * @param {string} filePath
274
+ * @param {string} originalContent
275
+ * @param {string} fileDiff
276
+ * @param {string} model
277
+ * @param {object} [opts]
278
+ * @returns {Promise<string>} the cleaned file content
279
+ */
280
+ export async function callLlmForApplyWithThink(filePath, originalContent, fileDiff, model, opts = {}) {
281
+ const applyFn = opts.callLlmForApply || callLlmForApply;
282
+ const fullResponse = await applyFn(filePath, originalContent, fileDiff, model, opts);
283
+ return stripThinkTags(fullResponse);
284
+ }
285
+
286
+ /**
287
+ * Apply unified diffs to a map of file contents. Handles creations,
288
+ * modifications, and deletions. Returns a new map (the input is not mutated).
289
+ * Deleted files are omitted.
290
+ *
291
+ * Each file is processed in parallel. By default, every file is first applied
292
+ * deterministically (no LLM call) via {@link applyPatchToFile}; only files
293
+ * whose patch does not apply cleanly fall back to AI-powered conflict
294
+ * resolution. Set `opts.forceLlm` to skip the deterministic fast path and send
295
+ * every file to the LLM.
296
+ *
297
+ * @param {string} diffText
298
+ * @param {Record<string, string>} files
299
+ * @param {object} [opts]
300
+ * @param {string} [opts.model]
301
+ * @param {string} [opts.apiKey]
302
+ * @param {string} [opts.baseUrl]
303
+ * @param {number} [opts.maxTokens]
304
+ * @param {boolean} [opts.forceLlm] skip the deterministic fast path
305
+ * @param {(text: string, path: string) => void} [opts.onToken] stream
306
+ * callback for LLM-resolved files. Files are processed concurrently, so
307
+ * token calls for different files interleave; the second argument is the
308
+ * file path the token belongs to, letting callers demultiplex. Files taken
309
+ * by the deterministic fast path emit no tokens (no LLM call is made).
310
+ * @param {Function} [opts.callLlmForApply] injectable single-file applier
311
+ * @returns {Promise<Record<string, string>>}
312
+ */
313
+ export async function smartapply(diffText, files, opts = {}) {
314
+ const model = resolveModel(opts.model);
315
+ const parsedDiffs = parseDiffPerFile(diffText);
316
+ const result = { ...files };
317
+
318
+ const applyOpts = {
319
+ apiKey: opts.apiKey,
320
+ baseUrl: opts.baseUrl,
321
+ maxTokens: opts.maxTokens,
322
+ callLlmForApply: opts.callLlmForApply,
323
+ };
324
+
325
+ await Promise.all(
326
+ parsedDiffs.map(async ([path, patch]) => {
327
+ const original = Object.prototype.hasOwnProperty.call(result, path) ? result[path] : '';
328
+ if (patch.includes('+++ /dev/null')) {
329
+ delete result[path];
330
+ return;
331
+ }
332
+ // Fast path: try to apply the patch deterministically, no LLM call.
333
+ if (!opts.forceLlm) {
334
+ const deterministic = applyPatchToFile(original, patch);
335
+ if (deterministic !== null) {
336
+ result[path] = deterministic;
337
+ return;
338
+ }
339
+ }
340
+ // Fallback: the patch did not apply cleanly, let the LLM resolve it.
341
+ // Bind the file path into the stream callback so concurrent per-file
342
+ // streams can be told apart by the caller: onToken(text, path).
343
+ const fileOpts = opts.onToken
344
+ ? { ...applyOpts, onToken: (text) => opts.onToken(text, path) }
345
+ : applyOpts;
346
+ const updated = await callLlmForApplyWithThink(path, original, patch, model, fileOpts);
347
+ result[path] = stripBadOutput(updated, original);
348
+ }),
349
+ );
350
+
351
+ return result;
352
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * as oauth from "./oauth.js";
2
+ export { generateDiff, smartapply, buildEnvironment, callLlmForDiff, callLlmForApply, callLlmForApplyWithThink } from "./gptdiff.js";
3
+ export { parseDiffPerFile, applyDiff, applyPatchToFile } from "./applydiff.js";
4
+ export { colorCodeDiff, swallowReasoning, stripBadOutput, extractDiffBlocks, stripThinkTags } from "./text.js";
5
+ export { callLlm, resolveApiKey, resolveBaseUrl } from "./llm.js";
6
+ export { getEnv, setEnv, DEFAULT_MODEL, DEFAULT_BASE_URL } from "./env.js";
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * gptdiff-js — generate and apply git diffs using LLMs, in the browser.
3
+ *
4
+ * Public API mirrors the gptdiff Python package, scoped to generateDiff +
5
+ * smartapply and their building blocks.
6
+ */
7
+
8
+ export { generateDiff, smartapply, buildEnvironment, callLlmForDiff, callLlmForApply, callLlmForApplyWithThink } from './gptdiff.js';
9
+ export { parseDiffPerFile, applyDiff, applyPatchToFile } from './applydiff.js';
10
+ export { colorCodeDiff, swallowReasoning, stripBadOutput, extractDiffBlocks, stripThinkTags } from './text.js';
11
+ export { callLlm, resolveApiKey, resolveBaseUrl } from './llm.js';
12
+ export { getEnv, setEnv, DEFAULT_MODEL, DEFAULT_BASE_URL } from './env.js';
13
+ export * as oauth from './oauth.js';
package/src/llm.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Call a chat-completions endpoint.
3
+ *
4
+ * @param {object} opts
5
+ * @param {string} [opts.apiKey]
6
+ * @param {string} [opts.baseUrl]
7
+ * @param {string} opts.model
8
+ * @param {Array<object>} opts.messages
9
+ * @param {number} [opts.maxTokens]
10
+ * @param {number} [opts.temperature]
11
+ * @param {number} [opts.budgetTokens] Anthropic extended-thinking budget
12
+ * @param {(text: string) => void} [opts.onToken] when provided, the request is
13
+ * made with `stream: true` and this is called once per content delta as
14
+ * tokens arrive. The resolved value keeps the exact non-streaming shape
15
+ * (`{ choices: [{ message: { content } }], usage }`), so callers work
16
+ * unchanged. Ignored on the Anthropic branch (which stays non-streaming).
17
+ * @param {typeof fetch} [opts.fetchImpl] override for testing
18
+ * @param {number} [opts.maxRetries] retries on transient 429/5xx (default 3)
19
+ * @param {number} [opts.retryBaseMs] base backoff in ms (default 1500, exponential, capped 30s)
20
+ * @returns {Promise<{choices: Array<{message: {content: string}}>, usage: object}>}
21
+ */
22
+ export function callLlm({ apiKey, baseUrl, model, messages, maxTokens, temperature, budgetTokens, onToken, fetchImpl, maxRetries, retryBaseMs, }: {
23
+ apiKey?: string;
24
+ baseUrl?: string;
25
+ model: string;
26
+ messages: Array<object>;
27
+ maxTokens?: number;
28
+ temperature?: number;
29
+ budgetTokens?: number;
30
+ onToken?: (text: string) => void;
31
+ fetchImpl?: typeof fetch;
32
+ maxRetries?: number;
33
+ retryBaseMs?: number;
34
+ }): Promise<{
35
+ choices: Array<{
36
+ message: {
37
+ content: string;
38
+ };
39
+ }>;
40
+ usage: object;
41
+ }>;
42
+ /**
43
+ * Resolve the effective API key, preferring an explicit override then the
44
+ * GPTDIFF_LLM_API_KEY environment variable.
45
+ * @param {string} [apiKey]
46
+ * @returns {string | undefined}
47
+ */
48
+ export function resolveApiKey(apiKey?: string): string | undefined;
49
+ /**
50
+ * Resolve the effective base URL, preferring an explicit override then the
51
+ * GPTDIFF_LLM_BASE_URL environment variable, then the NanoGPT default.
52
+ * @param {string} [baseUrl]
53
+ * @returns {string}
54
+ */
55
+ export function resolveBaseUrl(baseUrl?: string): string;
56
+ /**
57
+ * @param {string} baseUrl
58
+ * @returns {string}
59
+ */
60
+ export function domainForUrl(baseUrl: string): string;