refactor(opencode): migrate llm tests to layer nodes (#34479)

This commit is contained in:
James Long
2026-06-29 15:55:58 -04:00
committed by GitHub
parent b9dae8593c
commit 0ebe74b625
5 changed files with 78 additions and 135 deletions
@@ -1,6 +1,5 @@
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { HttpRecorder } from "@opencode-ai/http-recorder"
import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
@@ -10,14 +9,11 @@ import { Effect, Layer, Option, Schema, Stream } from "effect"
import path from "node:path"
import z from "zod"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { Filesystem } from "@/util/filesystem"
import { LLMEvent, LLMResponse } from "@opencode-ai/llm"
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
import { Env } from "@/env"
import { RequestExecutor } from "@opencode-ai/llm/route"
import { RuntimeFlags } from "@/effect/runtime-flags"
import type { Agent } from "../../src/agent/agent"
import { LLM } from "../../src/session/llm"
@@ -26,6 +22,9 @@ import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings")
@@ -240,7 +239,7 @@ const redactRecordedBody = (body: string) =>
function authLayer(scenario: RecordedScenario) {
const replayAuth = shouldRecord ? scenario.recordAuth?.() : scenario.replayAuth
if (!replayAuth) return Auth.defaultLayer
if (!replayAuth) return undefined
return Layer.mock(Auth.Service)({
get: (providerID) => Effect.succeed(providerID === scenario.providerID ? replayAuth : undefined),
all: () => Effect.succeed({ [scenario.providerID]: replayAuth }),
@@ -262,15 +261,6 @@ const modelsFixture = Filesystem.readJson<Record<string, ModelsDev.Provider>>(
function recordedNativeLLMLayer(scenario: RecordedScenario) {
const auth = authLayer(scenario)
const provider = Provider.layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(auth),
Layer.provide(Plugin.defaultLayer),
Layer.provide(ModelsDev.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
// Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
const metadata = {
provider: scenario.providerID,
@@ -290,21 +280,11 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
redactor: HttpRecorderInternal.Redactor.make(redact),
})
: HttpRecorder.http(scenario.cassette, { directory: FIXTURES_DIR, metadata, redact })
const recordedClient = LLMClient.layer.pipe(
Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)),
)
return Layer.mergeAll(
provider,
LLM.layer.pipe(
Layer.provide(auth),
Layer.provide(Config.defaultLayer),
Layer.provide(provider),
Layer.provide(Plugin.defaultLayer),
Layer.provide(recordedClient),
Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: true })),
),
)
return AppNodeBuilder.build(LayerNode.group([Provider.node, LLM.node]), [
[LayerNodePlatform.requestExecutor, RequestExecutor.layer.pipe(Layer.provide(recordedHttp))],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: true })],
...(auth ? ([[Auth.node, auth]] as const) : []),
])
}
const writeConfig = (directory: string, scenario: RecordedScenario, model: ModelsDev.Provider["models"][string]) =>
+22 -29
View File
@@ -9,13 +9,10 @@ import { InstanceRef } from "../../src/effect/instance-ref"
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import z from "zod"
import { LLM } from "../../src/session/llm"
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Plugin } from "@/plugin"
import { testEffect } from "../lib/effect"
import type { Agent } from "../../src/agent/agent"
@@ -27,6 +24,9 @@ import { LLMAISDK } from "@/session/llm/ai-sdk"
import { Session as SessionNs } from "@/session/session"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
type ConfigModel = NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
@@ -52,15 +52,13 @@ const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: stri
}
}
const it = testEffect(Layer.mergeAll(LLM.defaultLayer, Provider.defaultLayer))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([LLM.node, Provider.node])))
// LLM.stream returns a Stream, not an Effect, so we can't use the serviceUse proxy.
const drain = (input: LLM.StreamInput) => LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain))
// drainWith builds an isolated runtime so the custom layer fully owns LLM and
// its transitive deps — `Effect.provide(layer)` over an existing runtime layers
// the new services on top, but transitive Service overrides (e.g. RequestExecutor)
// resolved through the outer LLM.defaultLayer leak through.
// drainWith builds an isolated runtime so custom replacements fully own LLM and
// its transitive deps.
const drainWith = (layer: Layer.Layer<LLM.Service>, input: LLM.StreamInput) =>
Effect.gen(function* () {
const ctx = yield* InstanceRef
@@ -75,15 +73,14 @@ const drainWith = (layer: Layer.Layer<LLM.Service>, input: LLM.StreamInput) =>
)
})
function llmLayerWithExecutor(executor: Layer.Layer<RequestExecutor.Service>, flags: Partial<RuntimeFlags.Info> = {}) {
return LLM.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(executor, WebSocketExecutor.layer)))),
Layer.provide(RuntimeFlags.layer(flags)),
)
function llmLayerWithExecutor(options: {
executor?: Layer.Layer<RequestExecutor.Service>
flags?: Partial<RuntimeFlags.Info>
} = {}) {
return AppNodeBuilder.build(LLM.node, [
[RuntimeFlags.node, RuntimeFlags.layer(options.flags)],
...(options.executor ? ([[LayerNodePlatform.requestExecutor, options.executor]] as const) : []),
])
}
describe("session.llm.hasToolCalls", () => {
@@ -1129,14 +1126,10 @@ describe("session.llm.stream", () => {
} satisfies Agent.Info
yield* drainWith(
LLM.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(failingNativeClient),
Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: false })),
),
AppNodeBuilder.build(LLM.node, [
[LayerNodePlatform.llmClient, failingNativeClient],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: false })],
]),
{
user: {
id: MessageID.make("msg_user-native-flag-off"),
@@ -1199,7 +1192,7 @@ describe("session.llm.stream", () => {
temperature: 0.2,
} satisfies Agent.Info
yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), {
yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), {
user: {
id: MessageID.make("msg_user-native"),
sessionID,
@@ -1282,7 +1275,7 @@ describe("session.llm.stream", () => {
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
yield* drainWith(llmLayerWithExecutor(executor, { experimentalNativeLlm: true }), {
yield* drainWith(llmLayerWithExecutor({ executor, flags: { experimentalNativeLlm: true } }), {
user: {
id: MessageID.make("msg_user-native-injected-tool"),
sessionID,
@@ -1371,7 +1364,7 @@ describe("session.llm.stream", () => {
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), {
yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), {
user: {
id: MessageID.make("msg_user-native-tool"),
sessionID,