From 04ef48ae60dd6709c2541617bed65aa0e396f4fe Mon Sep 17 00:00:00 2001 From: zidou-kiyn <3038771237@qq.com> Date: Wed, 30 Sep 2026 23:52:16 +0800 Subject: [PATCH 1/2] feat: declare the Codex grammar through pi's constrainedSampling API The freeform property on the tool definition is not read by pi, so apply_patch always went out as a plain JSON function tool, even on providers that support OpenAI grammar tools. Pi's public ToolDefinition.constrainedSampling hook takes grammar variants. Declaring the Codex Lark grammar there sends apply_patch as a native OpenAI custom tool whenever the model's provider sets compat.supportsOpenAIGrammarTools (pi's bundled OpenAI and OpenAI Codex catalogs do), and falls back to the function tool everywhere else. The freeform property is kept for existing consumers. --- src/index.ts | 4 ++++ test/index.test.ts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/index.ts b/src/index.ts index 3e98659..c38a056 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1415,6 +1415,10 @@ export function createApplyPatchTool(): ApplyPatchToolDefinition { label: "ApplyPatch", description: APPLY_PATCH_FREEFORM_DESCRIPTION, parameters: APPLY_PATCH_PARAMS, + // Pi's public grammar hook: models whose provider sets `compat.supportsOpenAIGrammarTools` + // receive apply_patch as a native OpenAI custom tool constrained by the Codex Lark grammar. + // Other providers fall back to the plain function tool. + constrainedSampling: { type: "grammar", variants: { openai_lark: APPLY_PATCH_LARK_GRAMMAR } }, prepareArguments: normalizeApplyPatchArguments, promptSnippet: "Apply Codex-format file patches with apply_patch", promptGuidelines: [ diff --git a/test/index.test.ts b/test/index.test.ts index c8e22be..4759881 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -112,11 +112,13 @@ describe("pi-apply-patch", () => { let capturedToolName: string | undefined; let capturedDescription: string | undefined; let capturedFreeform: FreeformToolFormat | undefined; + let capturedConstrainedSampling: ReturnType["constrainedSampling"]; const extensionApi = { registerTool(tool: ReturnType) { capturedToolName = tool.name; capturedDescription = tool.description; capturedFreeform = tool.freeform; + capturedConstrainedSampling = tool.constrainedSampling; }, on() { return () => {}; @@ -138,6 +140,10 @@ describe("pi-apply-patch", () => { syntax: "lark", definition: APPLY_PATCH_LARK_GRAMMAR, }); + expect(capturedConstrainedSampling).toEqual({ + type: "grammar", + variants: { openai_lark: APPLY_PATCH_LARK_GRAMMAR }, + }); }); it("#given GPT model after reload with apply_patch already active #when session starts #then keeps apply_patch active", async () => { From 8f70b8008b1d8674ea0986ee05dea698e1c8cc68 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 3 Oct 2026 21:15:27 +0900 Subject: [PATCH 2/2] test: verify the grammar tool through pi's constrained-sampling resolver Replace the assertion that pinned the constrainedSampling object with tests that run pi's public resolver (@earendil-works/pi-ai/api/constrained-sampling): a provider with OpenAI grammar tools gets the Codex Lark grammar bound to the 'input' parameter, and any other provider keeps the plain function tool. Add the CHANGELOG entry. --- CHANGELOG.md | 1 + test/grammar-tool.test.ts | 29 +++++++++++++++++++++++++++++ test/index.test.ts | 6 ------ 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 test/grammar-tool.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1debc8a..3641ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Recognize indented file headers like Codex, and reject any other line between file sections instead of silently dropping that section while reporting success (#45). - Preserve each line's ending on update: CRLF and mixed-ending files are no longer rewritten to LF, inserted lines take the file's line ending, and context lines keep their exact text (#47). +- Declare the Codex Lark grammar through pi's `constrainedSampling` hook, so GPT models on providers with OpenAI grammar tools get `apply_patch` as a native grammar tool instead of a plain JSON function (#43, thanks @zidou-kiyn). ### Changed diff --git a/test/grammar-tool.test.ts b/test/grammar-tool.test.ts new file mode 100644 index 0000000..1f3b843 --- /dev/null +++ b/test/grammar-tool.test.ts @@ -0,0 +1,29 @@ +import { resolveGrammarConstrainedSampling } from "@earendil-works/pi-ai/api/constrained-sampling"; +import { describe, expect, it } from "vitest"; +import { APPLY_PATCH_LARK_GRAMMAR, createApplyPatchTool } from "../src/index.js"; + +describe("apply_patch grammar tool", () => { + it("#given a provider with OpenAI grammar tools #when pi prepares apply_patch #then it is sent as the Codex Lark grammar tool", () => { + // given + const tool = createApplyPatchTool(); + + // when + const grammar = resolveGrammarConstrainedSampling(tool, true); + + // then + expect(grammar?.format).toBe("lark"); + expect(grammar?.definition).toBe(APPLY_PATCH_LARK_GRAMMAR); + expect(grammar?.inputProperty).toBe("input"); + }); + + it("#given a provider without grammar tools #when pi prepares apply_patch #then it stays a plain function tool", () => { + // given + const tool = createApplyPatchTool(); + + // when + const grammar = resolveGrammarConstrainedSampling(tool, false); + + // then + expect(grammar).toBeUndefined(); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts index 4759881..c8e22be 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -112,13 +112,11 @@ describe("pi-apply-patch", () => { let capturedToolName: string | undefined; let capturedDescription: string | undefined; let capturedFreeform: FreeformToolFormat | undefined; - let capturedConstrainedSampling: ReturnType["constrainedSampling"]; const extensionApi = { registerTool(tool: ReturnType) { capturedToolName = tool.name; capturedDescription = tool.description; capturedFreeform = tool.freeform; - capturedConstrainedSampling = tool.constrainedSampling; }, on() { return () => {}; @@ -140,10 +138,6 @@ describe("pi-apply-patch", () => { syntax: "lark", definition: APPLY_PATCH_LARK_GRAMMAR, }); - expect(capturedConstrainedSampling).toEqual({ - type: "grammar", - variants: { openai_lark: APPLY_PATCH_LARK_GRAMMAR }, - }); }); it("#given GPT model after reload with apply_patch already active #when session starts #then keeps apply_patch active", async () => {