diff --git a/packages/opencode/test/session/llm-native-recorded.test.ts b/packages/opencode/test/session/llm-native-recorded.test.ts index b79e12957d..23b4a1dc76 100644 --- a/packages/opencode/test/session/llm-native-recorded.test.ts +++ b/packages/opencode/test/session/llm-native-recorded.test.ts @@ -1,6 +1,6 @@ import { NodeFileSystem } from "@effect/platform-node" import { HttpRecorder } from "@opencode-ai/http-recorder" -import { describe, expect, test } from "bun:test" +import { describe, expect } from "bun:test" import { tool } from "ai" import { Effect, Layer, Stream } from "effect" import { FetchHttpClient } from "effect/unstable/http" @@ -8,20 +8,18 @@ import path from "node:path" import z from "zod" import { Auth } from "@/auth" import { Config } from "@/config/config" -import { AppRuntime } from "@/effect/app-runtime" -import { attach } from "@/effect/run-service" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Filesystem } from "@/util/filesystem" import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route" import type { Agent } from "../../src/agent/agent" -import { WithInstance } from "../../src/project/with-instance" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, SessionID } from "../../src/session/schema" import type { ModelsDev } from "../../src/provider/models" -import { tmpdir } from "../fixture/fixture" +import { TestInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" const CASSETTE = "session/native-openai-tool-call" const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings") @@ -31,7 +29,6 @@ const shouldRecord = process.env.RECORD === "true" const canRun = shouldRecord ? Boolean(OPENAI_API_KEY) : HttpRecorder.hasCassetteSync(CASSETTE, { directory: FIXTURES_DIR }) -const recordedTest = canRun ? test : test.skip async function loadFixture(providerID: string, modelID: string) { const data = await Filesystem.readJson>( @@ -44,15 +41,6 @@ async function loadFixture(providerID: string, modelID: string) { return model } -async function getModel(providerID: ProviderID, modelID: ModelID) { - return AppRuntime.runPromise( - Effect.gen(function* () { - const provider = yield* Provider.Service - return yield* provider.getModel(providerID, modelID) - }), - ) -} - const openAIConfig = (model: ModelsDev.Provider["models"][string]): Partial => ({ enabled_providers: ["openai"], provider: { @@ -74,10 +62,11 @@ const openAIConfig = (model: ModelsDev.Provider["models"][string]): Partial, input: LLM.StreamInput) { - return Array.from( - await Effect.runPromise( - attach(LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runCollect))).pipe(Effect.provide(layer)), +const it = testEffect(recordedNativeLLMLayer()) +const recordedInstance = canRun ? it.instance : it.instance.skip + +const writeConfig = (directory: string, model: ModelsDev.Provider["models"][string]) => + Effect.promise(() => + Bun.write( + path.join(directory, "opencode.json"), + JSON.stringify({ $schema: "https://opencode.ai/config.json", ...openAIConfig(model) }), ), ) + +const getModel = (providerID: ProviderID, modelID: ModelID) => + Effect.gen(function* () { + const provider = yield* Provider.Service + return yield* provider.getModel(providerID, modelID) + }) + +const collect = (input: LLM.StreamInput) => + Effect.gen(function* () { + const llm = yield* LLM.Service + return Array.from(yield* llm.stream(input).pipe(Stream.runCollect)) + }) + +const nativeRuntime = (effect: Effect.Effect) => { + return Effect.acquireUseRelease( + Effect.sync(() => { + const previous = process.env.OPENCODE_LLM_RUNTIME + process.env.OPENCODE_LLM_RUNTIME = "native" + return previous + }), + () => effect, + (previous) => + Effect.sync(() => { + if (previous === undefined) delete process.env.OPENCODE_LLM_RUNTIME + else process.env.OPENCODE_LLM_RUNTIME = previous + }), + ) } describe("session.llm native recorded", () => { - recordedTest("uses real RequestExecutor with HTTP recorder for native OpenAI tools", async () => { - const model = await loadFixture("openai", "gpt-4.1-mini") - let executed: unknown + recordedInstance("uses real RequestExecutor with HTTP recorder for native OpenAI tools", () => + Effect.gen(function* () { + const test = yield* TestInstance + const model = yield* Effect.promise(() => loadFixture("openai", "gpt-4.1-mini")) + yield* writeConfig(test.directory, model) - await using tmp = await tmpdir({ config: openAIConfig(model) }) + const sessionID = SessionID.make("session-recorded-native-tool") + const agent = { + name: "test", + mode: "primary", + prompt: "Call tools exactly as instructed.", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + temperature: 0, + } satisfies Agent.Info + const resolved = yield* getModel(ProviderID.openai, ModelID.make(model.id)) + let executed: unknown - await WithInstance.provide({ - directory: tmp.path, - fn: async () => { - const previous = process.env.OPENCODE_LLM_RUNTIME - process.env.OPENCODE_LLM_RUNTIME = "native" - try { - const sessionID = SessionID.make("session-recorded-native-tool") - const agent = { - name: "test", - mode: "primary", - prompt: "Call tools exactly as instructed.", - options: {}, - permission: [{ permission: "*", pattern: "*", action: "allow" }], - temperature: 0, - } satisfies Agent.Info - - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id)) - const events = await collect(recordedLLMLayer(), { - user: { - id: MessageID.make("msg_user-recorded-native-tool"), - sessionID, - role: "user", - time: { created: 0 }, - agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: ModelID.make(model.id) }, - } satisfies MessageV2.User, + const events = yield* nativeRuntime( + collect({ + user: { + id: MessageID.make("msg_user-recorded-native-tool"), sessionID, - model: resolved, - agent, - system: ["You must call the lookup tool exactly once with query weather. Do not answer in text."], - messages: [{ role: "user", content: "Use lookup." }], - toolChoice: "required", - tools: { - lookup: tool({ - description: "Lookup data.", - inputSchema: z.object({ query: z.string() }), - execute: async (args, options) => { - executed = { args, toolCallId: options.toolCallId } - return { output: "looked up" } - }, - }), - }, - }) + role: "user", + time: { created: 0 }, + agent: agent.name, + model: { providerID: ProviderID.make("openai"), modelID: ModelID.make(model.id) }, + } satisfies MessageV2.User, + sessionID, + model: resolved, + agent, + system: ["You must call the lookup tool exactly once with query weather. Do not answer in text."], + messages: [{ role: "user", content: "Use lookup." }], + toolChoice: "required", + tools: { + lookup: tool({ + description: "Lookup data.", + inputSchema: z.object({ query: z.string() }), + execute: async (args, options) => { + executed = { args, toolCallId: options.toolCallId } + return { output: "looked up" } + }, + }), + }, + }), + ) - expect(events.filter((event) => event.type === "step-finish")).toHaveLength(1) - expect(events.filter((event) => event.type === "finish")).toHaveLength(1) - expect(events.some((event) => event.type === "tool-result")).toBe(true) - expect(executed).toMatchObject({ args: { query: "weather" }, toolCallId: expect.any(String) }) - } finally { - if (previous === undefined) delete process.env.OPENCODE_LLM_RUNTIME - else process.env.OPENCODE_LLM_RUNTIME = previous - } - }, - }) - }) + expect(events.filter((event) => event.type === "step-finish")).toHaveLength(1) + expect(events.filter((event) => event.type === "finish")).toHaveLength(1) + expect(events.some((event) => event.type === "tool-result")).toBe(true) + expect(executed).toMatchObject({ args: { query: "weather" }, toolCallId: expect.any(String) }) + }), + ) })