Skip to content

Commit 4628afd

Browse files
feat(openai): add support for apply patch tool
1 parent 26e23c5 commit 4628afd

File tree

6 files changed

+337
-0
lines changed

6 files changed

+337
-0
lines changed

.changeset/poor-dogs-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/openai": minor
3+
---
4+
5+
feat(openai): add support for apply patch tool

libs/providers/langchain-openai/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,70 @@ const response = await llmWithShell.invoke(
508508

509509
For more information, see [OpenAI's Local Shell Documentation](https://platform.openai.com/docs/guides/tools-local-shell).
510510

511+
### Apply Patch Tool
512+
513+
The Apply Patch tool allows models to propose structured diffs that your integration applies. This enables iterative, multi-step code editing workflows where the model can create, update, and delete files in your codebase.
514+
515+
**When to use**:
516+
517+
- **Multi-file refactors** – Rename symbols, extract helpers, or reorganize modules
518+
- **Bug fixes** – Have the model both diagnose issues and emit precise patches
519+
- **Tests & docs generation** – Create new test files, fixtures, and documentation
520+
- **Migrations & mechanical edits** – Apply repetitive, structured updates
521+
522+
> **Security Warning**: Applying patches can modify files in your codebase. Always validate paths, implement backups, and consider sandboxing.
523+
> **Note**: This tool is designed to work with `gpt-5.1` model.
524+
525+
```typescript
526+
import { ChatOpenAI, tools } from "@langchain/openai";
527+
import { applyDiff } from "@openai/agents";
528+
import * as fs from "fs/promises";
529+
530+
const model = new ChatOpenAI({ model: "gpt-5.1" });
531+
532+
// With execute callback for automatic patch handling
533+
const patchTool = tools.applyPatch({
534+
execute: async (operation) => {
535+
if (operation.type === "create_file") {
536+
const content = applyDiff("", operation.diff, "create");
537+
await fs.writeFile(operation.path, content);
538+
return `Created ${operation.path}`;
539+
}
540+
if (operation.type === "update_file") {
541+
const current = await fs.readFile(operation.path, "utf-8");
542+
const newContent = applyDiff(current, operation.diff);
543+
await fs.writeFile(operation.path, newContent);
544+
return `Updated ${operation.path}`;
545+
}
546+
if (operation.type === "delete_file") {
547+
await fs.unlink(operation.path);
548+
return `Deleted ${operation.path}`;
549+
}
550+
return "Unknown operation type";
551+
},
552+
});
553+
554+
const llmWithPatch = model.bindTools([patchTool]);
555+
const response = await llmWithPatch.invoke(
556+
"Rename the fib() function to fibonacci() in lib/fib.py"
557+
);
558+
```
559+
560+
**Operation types**: The model returns operations with these properties:
561+
562+
- `create_file` – Create a new file at `path` with content from `diff`
563+
- `update_file` – Modify an existing file at `path` using V4A diff format in `diff`
564+
- `delete_file` – Remove a file at `path`
565+
566+
**Best practices**:
567+
568+
- **Path validation**: Prevent directory traversal and restrict edits to allowed directories
569+
- **Backups**: Consider backing up files before applying patches
570+
- **Error handling**: Return descriptive error messages so the model can recover
571+
- **Atomicity**: Decide whether you want "all-or-nothing" semantics (rollback if any patch fails)
572+
573+
For more information, see [OpenAI's Apply Patch Documentation](https://platform.openai.com/docs/guides/tools-apply-patch).
574+
511575
## Embeddings
512576

513577
This package also adds support for OpenAI's embeddings model.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { OpenAI as OpenAIClient } from "openai";
2+
import { tool } from "@langchain/core/tools";
3+
4+
/**
5+
* Re-export operation types from OpenAI SDK for convenience.
6+
*/
7+
export type ApplyPatchCreateFileOperation =
8+
OpenAIClient.Responses.ResponseApplyPatchToolCall.CreateFile;
9+
export type ApplyPatchUpdateFileOperation =
10+
OpenAIClient.Responses.ResponseApplyPatchToolCall.UpdateFile;
11+
export type ApplyPatchDeleteFileOperation =
12+
OpenAIClient.Responses.ResponseApplyPatchToolCall.DeleteFile;
13+
14+
/**
15+
* Union type of all apply patch operations from OpenAI SDK.
16+
*/
17+
export type ApplyPatchOperation = NonNullable<
18+
OpenAIClient.Responses.ResponseApplyPatchToolCall["operation"]
19+
>;
20+
21+
/**
22+
* Options for the Apply Patch tool.
23+
*/
24+
export interface ApplyPatchOptions {
25+
/**
26+
* Execute function that handles patch operations.
27+
* This function receives the operation input and should return a string
28+
* describing the result (success or failure message).
29+
*
30+
* The operation types are:
31+
* - `create_file`: Create a new file at the specified path with the diff content
32+
* - `update_file`: Modify an existing file using V4A diff format
33+
* - `delete_file`: Remove a file at the specified path
34+
*
35+
* @example
36+
* ```typescript
37+
* execute: async (operation) => {
38+
* if (operation.type === "create_file") {
39+
* const content = applyDiff("", operation.diff, "create");
40+
* await fs.writeFile(operation.path, content);
41+
* return `Created ${operation.path}`;
42+
* }
43+
* if (operation.type === "update_file") {
44+
* const current = await fs.readFile(operation.path, "utf-8");
45+
* const newContent = applyDiff(current, operation.diff);
46+
* await fs.writeFile(operation.path, newContent);
47+
* return `Updated ${operation.path}`;
48+
* }
49+
* if (operation.type === "delete_file") {
50+
* await fs.unlink(operation.path);
51+
* return `Deleted ${operation.path}`;
52+
* }
53+
* return "Unknown operation type";
54+
* }
55+
* ```
56+
*/
57+
execute: (operation: ApplyPatchOperation) => string | Promise<string>;
58+
}
59+
60+
/**
61+
* OpenAI Apply Patch tool type for the Responses API.
62+
*/
63+
export type ApplyPatchTool = OpenAIClient.Responses.ApplyPatchTool;
64+
65+
const TOOL_NAME = "apply_patch";
66+
67+
/**
68+
* Creates an Apply Patch tool that allows models to propose structured diffs
69+
* that your integration applies. This enables iterative, multi-step code
70+
* editing workflows.
71+
*
72+
* **Apply Patch** lets GPT-5.1 create, update, and delete files in your codebase
73+
* using structured diffs. Instead of just suggesting edits, the model emits
74+
* patch operations that your application applies and then reports back on.
75+
*
76+
* **When to use**:
77+
* - **Multi-file refactors** – Rename symbols, extract helpers, or reorganize modules
78+
* - **Bug fixes** – Have the model both diagnose issues and emit precise patches
79+
* - **Tests & docs generation** – Create new test files, fixtures, and documentation
80+
* - **Migrations & mechanical edits** – Apply repetitive, structured updates
81+
*
82+
* **How it works**:
83+
* The tool operates in a continuous loop:
84+
* 1. Model sends patch operations (`apply_patch_call` with operation type)
85+
* 2. Your code applies the patch to your working directory or repo
86+
* 3. You return success/failure status and optional output
87+
* 4. Repeat until the task is complete
88+
*
89+
* **Security Warning**: Applying patches can modify files in your codebase.
90+
* Always validate paths, implement backups, and consider sandboxing.
91+
*
92+
* @see {@link https://platform.openai.com/docs/guides/tools-apply-patch | OpenAI Apply Patch Documentation}
93+
*
94+
* @param options - Configuration options for the Apply Patch tool
95+
* @returns An Apply Patch tool that can be passed to `bindTools`
96+
*
97+
* @example
98+
* ```typescript
99+
* import { ChatOpenAI, tools } from "@langchain/openai";
100+
* import { applyDiff } from "@openai/agents";
101+
* import * as fs from "fs/promises";
102+
*
103+
* const model = new ChatOpenAI({ model: "gpt-5.1" });
104+
*
105+
* // With execute callback for automatic patch handling
106+
* const patchTool = tools.applyPatch({
107+
* execute: async (operation) => {
108+
* if (operation.type === "create_file") {
109+
* const content = applyDiff("", operation.diff, "create");
110+
* await fs.writeFile(operation.path, content);
111+
* return `Created ${operation.path}`;
112+
* }
113+
* if (operation.type === "update_file") {
114+
* const current = await fs.readFile(operation.path, "utf-8");
115+
* const newContent = applyDiff(current, operation.diff);
116+
* await fs.writeFile(operation.path, newContent);
117+
* return `Updated ${operation.path}`;
118+
* }
119+
* if (operation.type === "delete_file") {
120+
* await fs.unlink(operation.path);
121+
* return `Deleted ${operation.path}`;
122+
* }
123+
* return "Unknown operation type";
124+
* },
125+
* });
126+
*
127+
* const llmWithPatch = model.bindTools([patchTool]);
128+
* const response = await llmWithPatch.invoke(
129+
* "Rename the fib() function to fibonacci() in lib/fib.py"
130+
* );
131+
* ```
132+
*
133+
* @remarks
134+
* - Only available through the Responses API (not Chat Completions)
135+
* - Designed for use with `gpt-5.1` model
136+
* - Operations include: `create_file`, `update_file`, `delete_file`
137+
* - Patches use V4A diff format for updates
138+
* - Always validate paths to prevent directory traversal attacks
139+
* - Consider backing up files before applying patches
140+
* - Implement "all-or-nothing" semantics if atomicity is required
141+
*/
142+
export function applyPatch(options: ApplyPatchOptions) {
143+
const patchTool = tool(options.execute, {
144+
name: TOOL_NAME,
145+
description:
146+
"Apply structured diffs to create, update, or delete files in the codebase.",
147+
schema: {
148+
type: "object",
149+
properties: {
150+
operation: {
151+
type: "string",
152+
enum: ["create_file", "update_file", "delete_file"],
153+
},
154+
},
155+
required: ["operation"],
156+
},
157+
});
158+
159+
patchTool.extras = {
160+
...(patchTool.extras ?? {}),
161+
providerToolDefinition: {
162+
type: "apply_patch",
163+
} as ApplyPatchTool,
164+
};
165+
166+
return patchTool;
167+
}

libs/providers/langchain-openai/src/tools/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ export type {
6868
LocalShellAction,
6969
} from "./localShell.js";
7070

71+
import { applyPatch } from "./applyPatch.js";
72+
export type {
73+
ApplyPatchTool,
74+
ApplyPatchOptions,
75+
ApplyPatchOperation,
76+
ApplyPatchCreateFileOperation,
77+
ApplyPatchUpdateFileOperation,
78+
ApplyPatchDeleteFileOperation,
79+
} from "./applyPatch.js";
80+
7181
export const tools = {
7282
webSearch,
7383
mcp,
@@ -76,4 +86,5 @@ export const tools = {
7686
imageGeneration,
7787
computerUse,
7888
localShell,
89+
applyPatch,
7990
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expectTypeOf, it, describe } from "vitest";
2+
import { tools } from "../index.js";
3+
4+
describe("OpenAI Apply Patch Tool Type Tests", () => {
5+
it("applyPatch execute receives correct operation types", () => {
6+
tools.applyPatch({
7+
execute: async (operation) => {
8+
expectTypeOf(operation.type).toEqualTypeOf<
9+
"create_file" | "update_file" | "delete_file"
10+
>();
11+
expectTypeOf(operation.path).toEqualTypeOf<string>();
12+
return "done";
13+
},
14+
});
15+
});
16+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { expect, it, describe } from "vitest";
3+
import { tools } from "../index.js";
4+
5+
describe("OpenAI Apply Patch Tool Tests", () => {
6+
it("applyPatch creates valid tool definitions", () => {
7+
const patch = tools.applyPatch({
8+
execute: async () => "done",
9+
});
10+
11+
expect(patch.name).toBe("apply_patch");
12+
expect(patch.extras?.providerToolDefinition).toMatchObject({
13+
type: "apply_patch",
14+
});
15+
});
16+
17+
it("applyPatch execute callback receives operation correctly", async () => {
18+
const operations: Array<{ type: string; path?: string; diff?: string }> =
19+
[];
20+
21+
const patch = tools.applyPatch({
22+
execute: async (operation) => {
23+
operations.push(operation);
24+
return `Processed ${operation.path}`;
25+
},
26+
});
27+
28+
// Directly call the execute function
29+
const createOp = {
30+
type: "create_file" as const,
31+
path: "test.txt",
32+
diff: "+hello world",
33+
};
34+
35+
// Access the underlying execute function from the tool
36+
const executeFunc = patch.func;
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38+
const result = await executeFunc(createOp as any);
39+
40+
expect(operations).toHaveLength(1);
41+
expect(operations[0].type).toBe("create_file");
42+
expect(operations[0].path).toBe("test.txt");
43+
expect(result).toBe("Processed test.txt");
44+
});
45+
46+
it("applyPatch handles all operation types", async () => {
47+
const operations: string[] = [];
48+
49+
const patch = tools.applyPatch({
50+
execute: async (operation) => {
51+
operations.push(operation.type);
52+
return "ok";
53+
},
54+
});
55+
56+
const executeFunc = patch.func;
57+
58+
await executeFunc({
59+
type: "create_file",
60+
path: "a.txt",
61+
diff: "+a",
62+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
63+
} as any);
64+
await executeFunc({
65+
type: "update_file",
66+
path: "b.txt",
67+
diff: "-x\n+y",
68+
} as any);
69+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
70+
await executeFunc({ type: "delete_file", path: "c.txt" } as any);
71+
72+
expect(operations).toEqual(["create_file", "update_file", "delete_file"]);
73+
});
74+
});

0 commit comments

Comments
 (0)