From 8b6219d83467d68e74b73a566dc6c1980ec2c9a3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 2 Apr 2026 14:10:26 -0400 Subject: [PATCH] fix(opencode): allow patch tools in e2e model routing --- packages/opencode/src/tool/registry.ts | 4 +- .../test/session/e2e-url-repro.test.ts | 85 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 133a5018ad..2620405144 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -32,6 +32,7 @@ import { pathToFileURL } from "url" import { Effect, Layer, ServiceMap } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" +import { Env } from "../env" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -166,7 +167,8 @@ export namespace ToolRegistry { } const usePatch = - model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4") + !!Env.get("OPENCODE_E2E_LLM_URL") || + (model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")) if (tool.id === "apply_patch") return usePatch if (tool.id === "edit" || tool.id === "write") return !usePatch diff --git a/packages/opencode/test/session/e2e-url-repro.test.ts b/packages/opencode/test/session/e2e-url-repro.test.ts index 411e88f625..744c7bf0bc 100644 --- a/packages/opencode/test/session/e2e-url-repro.test.ts +++ b/packages/opencode/test/session/e2e-url-repro.test.ts @@ -27,6 +27,7 @@ import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { Provider as ProviderSvc } from "../../src/provider/provider" import { ModelID, ProviderID } from "../../src/provider/schema" +import { Server } from "../../src/server/server" import { SessionCompaction } from "../../src/session/compaction" import { Instruction } from "../../src/session/instruction" import { SessionProcessor } from "../../src/session/processor" @@ -227,3 +228,87 @@ it.live("e2eURL routes apply_patch through mock server", () => }, ), ) + +it.live("server message route produces diff through mock server", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ dir, llm }) { + const prev = process.env.OPENCODE_E2E_LLM_URL + process.env.OPENCODE_E2E_LLM_URL = llm.url + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (prev === undefined) delete process.env.OPENCODE_E2E_LLM_URL + else process.env.OPENCODE_E2E_LLM_URL = prev + }), + ) + + const sessions = yield* Session.Service + const session = yield* sessions.create({ + title: "e2e route test", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + const app = Server.Default() + const patch = ["*** Begin Patch", "*** Add File: route-test.txt", "+line 1", "+line 2", "*** End Patch"].join( + "\n", + ) + + yield* llm.toolMatch( + (hit) => JSON.stringify(hit.body).includes("Your only valid response is one apply_patch tool call"), + "apply_patch", + { patchText: patch }, + ) + yield* llm.text("done") + + const res = yield* Effect.promise(() => + Promise.resolve( + app.request(`/session/${session.id}/message`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-opencode-directory": dir, + }, + body: JSON.stringify({ + agent: "build", + system: [ + "You are seeding deterministic e2e UI state.", + "Your only valid response is one apply_patch tool call.", + `Use this JSON input: ${JSON.stringify({ patchText: patch })}`, + "Do not call any other tools.", + "Do not output plain text.", + ].join("\n"), + parts: [{ type: "text", text: "Apply the provided patch exactly once." }], + }), + }), + ), + ) + expect(res.status).toBe(200) + yield* Effect.promise(() => res.json()) + + const calls = yield* llm.calls + expect(calls).toBe(2) + + const content = yield* Effect.promise(() => + Bun.file(`${dir}/route-test.txt`) + .text() + .catch(() => "NOT FOUND"), + ) + expect(content).toContain("line 1") + + let diff: Awaited> = [] + for (let i = 0; i < 30; i++) { + diff = yield* Effect.promise(() => SessionSummary.diff({ sessionID: session.id })) + if (diff.length > 0) break + yield* Effect.sleep("100 millis") + } + + expect(diff.length).toBeGreaterThan(0) + }), + { + git: true, + config: () => ({ + model: "openai/gpt-5.4", + agent: { build: { model: "openai/gpt-5.4" } }, + provider: { openai: { options: { apiKey: "test-openai-key" } } }, + }), + }, + ), +)