From 14366af069044640dfa963c6ca6c25008f259150 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 00:01:13 -0400 Subject: [PATCH] test(session): port llm.test.ts to it.instance (#28578) --- packages/opencode/src/session/llm.ts | 3 + packages/opencode/test/fixture/fixture.ts | 9 +- packages/opencode/test/lib/effect.ts | 2 +- packages/opencode/test/session/llm.test.ts | 1488 +++++++++----------- 4 files changed, 642 insertions(+), 860 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 8e2759cdb6..ec87bfc446 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,4 +1,5 @@ import { Provider } from "@/provider/provider" +import { serviceUse } from "@/effect/service-use" import * as Log from "@opencode-ai/core/util/log" import { Context, Effect, Layer } from "effect" import * as Stream from "effect/Stream" @@ -54,6 +55,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/LLM") {} +export const use = serviceUse(Service) + const live: Layer.Layer< Service, never, diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index 28b74a9189..5c3989d7ce 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -119,7 +119,7 @@ export async function tmpdir(options?: TmpDirOptions) { } /** Effectful scoped tmpdir. Cleaned up when the scope closes. Make sure these stay in sync */ -export function tmpdirScoped(options?: { git?: boolean; config?: Partial }) { +export function tmpdirScoped(options?: { git?: boolean; config?: Partial | (() => Partial) }) { return Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const dirpath = sanitizePath(path.join(os.tmpdir(), "opencode-test-" + Math.random().toString(36).slice(2))) @@ -146,10 +146,11 @@ export function tmpdirScoped(options?: { git?: boolean; config?: Partial fs.writeFile( path.join(dir, "opencode.json"), - JSON.stringify({ $schema: "https://opencode.ai/config.json", ...options.config }), + JSON.stringify({ $schema: "https://opencode.ai/config.json", ...resolved }), ), ) } @@ -180,7 +181,7 @@ export const disposeAllInstancesEffect = InstanceStore.Service.use((store) => st export function provideTmpdirInstance( self: (path: string) => Effect.Effect, - options?: { git?: boolean; config?: Partial }, + options?: { git?: boolean; config?: Partial | (() => Partial) }, ) { return Effect.gen(function* () { const path = yield* tmpdirScoped(options) @@ -204,7 +205,7 @@ export function provideTmpdirInstance( export class TestInstance extends Context.Service()("@test/Instance") {} export const withTmpdirInstance = - (options?: { git?: boolean; config?: Partial }) => + (options?: { git?: boolean; config?: Partial | (() => Partial) }) => (self: Effect.Effect) => Effect.gen(function* () { const directory = yield* tmpdirScoped(options) diff --git a/packages/opencode/test/lib/effect.ts b/packages/opencode/test/lib/effect.ts index f1fa5b9624..f04829601d 100644 --- a/packages/opencode/test/lib/effect.ts +++ b/packages/opencode/test/lib/effect.ts @@ -8,7 +8,7 @@ import type { Config } from "@/config/config" import { TestInstance, withTmpdirInstance } from "../fixture/fixture" type Body = Effect.Effect | (() => Effect.Effect) -type InstanceOptions = { git?: boolean; config?: Partial } +type InstanceOptions = { git?: boolean; config?: Partial | (() => Partial) } function isInstanceOptions(options: InstanceOptions | number | TestOptions | undefined): options is InstanceOptions { return !!options && typeof options === "object" && ("git" in options || "config" in options) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 869be6655a..876a53ff1a 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -1,13 +1,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test" import path from "path" import { tool, type ModelMessage } from "ai" -import { Cause, Effect, Exit, Layer, Stream } from "effect" +import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect" +import { InstanceRef } from "../../src/effect/instance-ref" import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import z from "zod" -import { makeRuntime } from "../../src/effect/run-service" -import { InstanceRef } from "../../src/effect/instance-ref" import { LLM } from "../../src/session/llm" -import type { InstanceContext } from "../../src/project/instance-context" import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" import { Auth } from "@/auth" import { Config } from "@/config/config" @@ -17,19 +15,19 @@ import { ModelsDev } from "@opencode-ai/core/models-dev" import { Plugin } from "@/plugin" import { ProviderID, ModelID } from "../../src/provider/schema" import { Filesystem } from "@/util/filesystem" -import { tmpdir, withTestInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" import type { Agent } from "../../src/agent/agent" import { MessageV2 } from "../../src/session/message-v2" import { SessionID, MessageID } from "../../src/session/schema" -import { AppRuntime } from "../../src/effect/app-runtime" import { RuntimeFlags } from "@/effect/runtime-flags" import { Permission } from "@/permission" import { LLMAISDK } from "@/session/llm/ai-sdk" import { Session as SessionNs } from "@/session/session" +type ConfigModel = NonNullable[string]["models"]>[string] + const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: string): Partial => { const { experimental: _experimental, ...configModel } = model - type ConfigModel = NonNullable[string]["models"]>[string] return { enabled_providers: ["openai"], provider: { @@ -50,31 +48,28 @@ const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: stri } } -async function getModel(providerID: ProviderID, modelID: ModelID, ctx: InstanceContext) { - const effect = Effect.gen(function* () { - const provider = yield* Provider.Service - return yield* provider.getModel(providerID, modelID) +const it = testEffect(Layer.mergeAll(LLM.defaultLayer, Provider.defaultLayer)) + +// 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. +const drainWith = (layer: Layer.Layer, input: LLM.StreamInput) => + Effect.gen(function* () { + const ctx = yield* InstanceRef + if (!ctx) return yield* Effect.die("InstanceRef not provided") + return yield* Effect.promise(() => + Effect.runPromise( + LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain)).pipe( + Effect.provide(layer), + Effect.provideService(InstanceRef, ctx), + ), + ), + ) }) - return AppRuntime.runPromise(effect.pipe(Effect.provideService(InstanceRef, ctx))) -} - -const llm = makeRuntime(LLM.Service, LLM.defaultLayer) - -async function drain(input: LLM.StreamInput, ctx: InstanceContext) { - return llm.runPromise((svc) => { - const effect = svc.stream(input).pipe(Stream.runDrain) - return effect.pipe(Effect.provideService(InstanceRef, ctx)) - }) -} - -async function drainWith(layer: Layer.Layer, input: LLM.StreamInput, ctx: InstanceContext) { - return Effect.runPromise( - LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain)).pipe( - Effect.provide(layer), - Effect.provideService(InstanceRef, ctx), - ), - ) -} function llmLayerWithExecutor(executor: Layer.Layer, flags: Partial = {}) { return LLM.layer.pipe( @@ -649,17 +644,15 @@ function createChatStream(text: string) { }) } -async function loadFixture(providerID: string, modelID: string) { - const fixturePath = path.join(import.meta.dir, "../tool/fixtures/models-api.json") - const data = await Filesystem.readJson>(fixturePath) - const provider = data[providerID] - if (!provider) { - throw new Error(`Missing provider in fixture: ${providerID}`) - } +const MODELS_FIXTURE = JSON.parse( + await Bun.file(path.join(import.meta.dir, "../tool/fixtures/models-api.json")).text(), +) as Record + +function loadFixture(providerID: string, modelID: string) { + const provider = MODELS_FIXTURE[providerID] + if (!provider) throw new Error(`Missing provider in fixture: ${providerID}`) const model = provider.models[modelID] - if (!model) { - throw new Error(`Missing model in fixture: ${modelID}`) - } + if (!model) throw new Error(`Missing model in fixture: ${modelID}`) return { provider, model } } @@ -705,49 +698,24 @@ function createEventResponse(chunks: unknown[], includeDone = false) { } describe("session.llm.stream", () => { - test("sends temperature, tokens, and reasoning options for openai-compatible models", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const providerID = "vivgrid" - const modelID = "gemini-3.1-pro-preview" - const fixture = await loadFixture(providerID, modelID) - const model = fixture.model - - const request = waitRequest( - "/chat/completions", - new Response(createChatStream("Hello"), { - status: 200, - headers: { "Content-Type": "text/event-stream" }, - }), - ) - - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: [providerID], - provider: { - [providerID]: { - options: { - apiKey: "test-key", - baseURL: `${server.url.origin}/v1`, - }, - }, - }, + const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" } + it.instance( + "sends temperature, tokens, and reasoning options for openai-compatible models", + () => + Effect.gen(function* () { + const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) + const request = waitRequest( + "/chat/completions", + new Response(createChatStream("Hello"), { + status: 200, + headers: { "Content-Type": "text/event-stream" }, }), ) - }, - }) - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel( + ProviderID.make(vivgridFixture.providerID), + ModelID.make(fixture.model.id), + ) const sessionID = SessionID.make("session-test-1") const agent = { name: "test", @@ -764,23 +732,20 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(providerID), modelID: resolved.id, variant: "high" }, + model: { providerID: ProviderID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" }, } satisfies MessageV2.User - await drain( - { - user, - sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }, - ctx, - ) + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) - const capture = await request + const capture = yield* Effect.promise(() => request) const body = capture.body const headers = capture.headers const url = capture.url @@ -800,44 +765,31 @@ describe("session.llm.stream", () => { const reasoning = (body.reasoningEffort as string | undefined) ?? (body.reasoning_effort as string | undefined) expect(reasoning).toBe("high") - }, - }) - }) + }), + { + config: () => ({ + enabled_providers: [vivgridFixture.providerID], + provider: { + [vivgridFixture.providerID]: { + options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + }), + }, + ) - test("service stream cancellation cancels provider response body promptly", async () => { - const server = state.server - if (!server) throw new Error("Server not initialized") + const alibabaQwenFixture = { providerID: "alibaba", modelID: "qwen-plus" } + it.instance( + "service stream cancellation cancels provider response body promptly", + () => + Effect.gen(function* () { + const fixture = loadFixture(alibabaQwenFixture.providerID, alibabaQwenFixture.modelID) + const pending = waitStreamingRequest("/chat/completions") - const providerID = "alibaba" - const modelID = "qwen-plus" - const fixture = await loadFixture(providerID, modelID) - const model = fixture.model - const pending = waitStreamingRequest("/chat/completions") - - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: [providerID], - provider: { - [providerID]: { - options: { - apiKey: "test-key", - baseURL: `${server.url.origin}/v1`, - }, - }, - }, - }), + const resolved = yield* Provider.use.getModel( + ProviderID.make(alibabaQwenFixture.providerID), + ModelID.make(fixture.model.id), ) - }, - }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx) const sessionID = SessionID.make("session-test-service-abort") const agent = { name: "test", @@ -851,83 +803,61 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, + model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id }, } satisfies MessageV2.User - const ctrl = new AbortController() - const run = llm.runPromiseExit( - (svc) => - svc - .stream({ - user, - sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }) - .pipe(Stream.runDrain, Effect.provideService(InstanceRef, ctx)), - { signal: ctrl.signal }, - ) + const fiber = yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }).pipe(Effect.exit, Effect.forkScoped) - await pending.request - ctrl.abort() + yield* Effect.promise(() => pending.request) + yield* Fiber.interrupt(fiber) - await Promise.race([pending.responseCanceled, timeout(500)]) - const exit = await run - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) { - expect(Cause.hasInterrupts(exit.cause)).toBe(true) + yield* Effect.promise(() => Promise.race([pending.responseCanceled, timeout(500)])) + const exit = yield* Fiber.await(fiber) + // Fiber.await returns an Exit>. Unwrap once. + const inner = Exit.isSuccess(exit) ? exit.value : exit + expect(Exit.isFailure(inner)).toBe(true) + if (Exit.isFailure(inner)) { + expect(Cause.hasInterrupts(inner.cause)).toBe(true) } - await Promise.race([pending.requestAborted, timeout(500)]).catch(() => undefined) - }, - }) - }) - - test("keeps tools enabled by prompt permissions", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const providerID = "alibaba" - const modelID = "qwen-plus" - const fixture = await loadFixture(providerID, modelID) - const model = fixture.model - - const request = waitRequest( - "/chat/completions", - new Response(createChatStream("Hello"), { - status: 200, - headers: { "Content-Type": "text/event-stream" }, + yield* Effect.promise(() => Promise.race([pending.requestAborted, timeout(500)]).catch(() => undefined)) }), - ) + { + config: () => ({ + enabled_providers: [alibabaQwenFixture.providerID], + provider: { + [alibabaQwenFixture.providerID]: { + options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + }), + }, + ) - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: [providerID], - provider: { - [providerID]: { - options: { - apiKey: "test-key", - baseURL: `${server.url.origin}/v1`, - }, - }, - }, + it.instance( + "keeps tools enabled by prompt permissions", + () => + Effect.gen(function* () { + const fixture = loadFixture(alibabaQwenFixture.providerID, alibabaQwenFixture.modelID) + const request = waitRequest( + "/chat/completions", + new Response(createChatStream("Hello"), { + status: 200, + headers: { "Content-Type": "text/event-stream" }, }), ) - }, - }) - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel( + ProviderID.make(alibabaQwenFixture.providerID), + ModelID.make(fixture.model.id), + ) const sessionID = SessionID.make("session-test-tools") const agent = { name: "test", @@ -942,47 +872,50 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, + model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id }, tools: { question: true }, } satisfies MessageV2.User - await drain( - { - user, - sessionID, - model: resolved, - agent, - permission: [{ permission: "question", pattern: "*", action: "allow" }], - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: { - question: tool({ - description: "Ask a question", - inputSchema: z.object({}), - execute: async () => ({ output: "" }), - }), - }, + yield* drain({ + user, + sessionID, + model: resolved, + agent, + permission: [{ permission: "question", pattern: "*", action: "allow" }], + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: { + question: tool({ + description: "Ask a question", + inputSchema: z.object({}), + execute: async () => ({ output: "" }), + }), }, - ctx, - ) + }) - const capture = await request + const capture = yield* Effect.promise(() => request) const tools = capture.body.tools as Array<{ function?: { name?: string } }> | undefined expect(tools?.some((item) => item.function?.name === "question")).toBe(true) - }, - }) - }) + }), + { + config: () => ({ + enabled_providers: [alibabaQwenFixture.providerID], + provider: { + [alibabaQwenFixture.providerID]: { + options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + }), + }, + ) - test("sends responses API payload for OpenAI models", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } + it.instance( + "sends responses API payload for OpenAI models", + () => + Effect.gen(function* () { + const model = loadFixture("openai", "gpt-5.2").model - const source = await loadFixture("openai", "gpt-5.5") - const model = source.model - - const responseChunks = [ + const responseChunks = [ { type: "response.created", response: { @@ -1024,14 +957,9 @@ describe("session.llm.stream", () => { }, }, ] - const request = waitRequest("/responses", createEventResponse(responseChunks, true)) + const request = waitRequest("/responses", createEventResponse(responseChunks, true)) - await using tmp = await tmpdir({ config: openAIConfig(model, `${server.url.origin}/v1`) }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) const sessionID = SessionID.make("session-test-2") const agent = { name: "test", @@ -1050,20 +978,17 @@ describe("session.llm.stream", () => { model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" }, } satisfies MessageV2.User - await drain( - { - user, - sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }, - ctx, - ) + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) - const capture = await request + const capture = yield* Effect.promise(() => request) const body = capture.body expect(capture.url.pathname.endsWith("/responses")).toBe(true) @@ -1073,81 +998,73 @@ describe("session.llm.stream", () => { const maxTokens = body.max_output_tokens as number | undefined expect(maxTokens).toBe(undefined) // match codex cli behavior - }, - }) - }) - - test("keeps supported OpenAI models on AI SDK path when native flag is off", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const source = await loadFixture("openai", "gpt-5.5") - const model = source.model - const request = waitRequest( - "/responses", - createEventResponse( - [ - { - type: "response.created", - response: { - id: "resp-flag-off", - created_at: Math.floor(Date.now() / 1000), - model: model.id, - service_tier: null, - }, - }, - { - type: "response.output_item.added", - output_index: 0, - item: { type: "message", id: "item-flag-off", status: "in_progress", role: "assistant", content: [] }, - }, - { - type: "response.content_part.added", - item_id: "item-flag-off", - output_index: 0, - content_index: 0, - part: { type: "output_text", text: "", annotations: [] }, - }, - { - type: "response.output_text.delta", - item_id: "item-flag-off", - delta: "Flag off", - logprobs: null, - }, - { - type: "response.completed", - response: { - incomplete_details: null, - usage: { - input_tokens: 1, - input_tokens_details: null, - output_tokens: 1, - output_tokens_details: null, - }, - service_tier: null, - }, - }, - ], - true, - ), - ) - const failingNativeClient = Layer.succeed( - LLMClient.Service, - LLMClient.Service.of({ - prepare: () => Effect.die(new Error("native LLM client should not be used when the flag is off")), - stream: () => Stream.die(new Error("native LLM client should not be used when the flag is off")), - generate: () => Effect.die(new Error("native LLM client should not be used when the flag is off")), }), - ) + { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) }, + ) - await using tmp = await tmpdir({ config: openAIConfig(model, `${server.url.origin}/v1`) }) + it.instance( + "keeps supported OpenAI models on AI SDK path when native flag is off", + () => + Effect.gen(function* () { + const model = loadFixture("openai", "gpt-5.2").model + const request = waitRequest( + "/responses", + createEventResponse( + [ + { + type: "response.created", + response: { + id: "resp-flag-off", + created_at: Math.floor(Date.now() / 1000), + model: model.id, + service_tier: null, + }, + }, + { + type: "response.output_item.added", + output_index: 0, + item: { type: "message", id: "item-flag-off", status: "in_progress", role: "assistant", content: [] }, + }, + { + type: "response.content_part.added", + item_id: "item-flag-off", + output_index: 0, + content_index: 0, + part: { type: "output_text", text: "", annotations: [] }, + }, + { + type: "response.output_text.delta", + item_id: "item-flag-off", + delta: "Flag off", + logprobs: null, + }, + { + type: "response.completed", + response: { + incomplete_details: null, + usage: { + input_tokens: 1, + input_tokens_details: null, + output_tokens: 1, + output_tokens_details: null, + }, + service_tier: null, + }, + }, + ], + true, + ), + ) + const failingNativeClient = Layer.succeed( + LLMClient.Service, + LLMClient.Service.of({ + prepare: () => Effect.die(new Error("native LLM client should not be used when the flag is off")), + stream: () => Stream.die(new Error("native LLM client should not be used when the flag is off")), + generate: () => Effect.die(new Error("native LLM client should not be used when the flag is off")), + }), + ) - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native-flag-off") const agent = { name: "test", @@ -1156,7 +1073,7 @@ describe("session.llm.stream", () => { permission: [{ permission: "*", pattern: "*", action: "allow" }], } satisfies Agent.Info - await drainWith( + yield* drainWith( LLM.layer.pipe( Layer.provide(Auth.defaultLayer), Layer.provide(Config.defaultLayer), @@ -1181,61 +1098,43 @@ describe("session.llm.stream", () => { messages: [{ role: "user", content: "Hello" }], tools: {}, }, - ctx, ) - const capture = await request + const capture = yield* Effect.promise(() => request) expect(capture.url.pathname.endsWith("/responses")).toBe(true) expect(capture.body.model).toBe(resolved.api.id) - }, - }) - }) + }), + { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) }, + ) - test("streams OpenAI through native runtime when opted in", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const source = await loadFixture("openai", "gpt-5.5") - const model = source.model - const chunks = [ - { - type: "response.created", - response: { - id: "resp-native", - }, - }, - { - type: "response.output_item.added", - item: { type: "message", id: "item-native", status: "in_progress" }, - }, - { - type: "response.output_text.delta", - item_id: "item-native", - delta: "Hello native", - }, - { - type: "response.completed", - response: { - incomplete_details: null, - usage: { - input_tokens: 1, - input_tokens_details: null, - output_tokens: 1, - output_tokens_details: null, + it.instance( + "streams OpenAI through native runtime when opted in", + () => + Effect.gen(function* () { + const model = loadFixture("openai", "gpt-5.2").model + const chunks = [ + { type: "response.created", response: { id: "resp-native" } }, + { + type: "response.output_item.added", + item: { type: "message", id: "item-native", status: "in_progress" }, }, - }, - }, - ] - const request = waitRequest("/responses", createEventResponse(chunks, true)) + { type: "response.output_text.delta", item_id: "item-native", delta: "Hello native" }, + { + type: "response.completed", + response: { + incomplete_details: null, + usage: { + input_tokens: 1, + input_tokens_details: null, + output_tokens: 1, + output_tokens_details: null, + }, + }, + }, + ] + const request = waitRequest("/responses", createEventResponse(chunks, true)) - await using tmp = await tmpdir({ config: openAIConfig(model, `${server.url.origin}/v1`) }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native") const agent = { name: "test", @@ -1245,28 +1144,24 @@ describe("session.llm.stream", () => { temperature: 0.2, } satisfies Agent.Info - await drainWith( - llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), - { - user: { - id: MessageID.make("msg_user-native"), - sessionID, - role: "user", - time: { created: Date.now() }, - agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" }, - } satisfies MessageV2.User, + yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), { + user: { + id: MessageID.make("msg_user-native"), sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }, - ctx, - ) + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" }, + } satisfies MessageV2.User, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) - const capture = await request + const capture = yield* Effect.promise(() => request) expect(capture.url.pathname.endsWith("/responses")).toBe(true) expect(capture.headers.get("Authorization")).toBe("Bearer test-openai-key") expect(capture.body.model).toBe(model.id) @@ -1274,58 +1169,55 @@ describe("session.llm.stream", () => { expect((capture.body.reasoning as { effort?: string } | undefined)?.effort).toBe("high") expect(JSON.stringify(capture.body.input)).toContain("You are a helpful assistant.") expect(capture.body.input).toContainEqual({ role: "user", content: [{ type: "input_text", text: "Hello" }] }) - }, - }) - }) - - test("uses injected native request executor for tool calls", async () => { - const source = await loadFixture("openai", "gpt-5.5") - const model = source.model - const chunks = [ - { - type: "response.output_item.added", - item: { type: "function_call", id: "item-injected-tool", call_id: "call-injected-tool", name: "lookup" }, - }, - { - type: "response.function_call_arguments.delta", - item_id: "item-injected-tool", - delta: '{"query":"weather"}', - }, - { - type: "response.output_item.done", - item: { - type: "function_call", - id: "item-injected-tool", - call_id: "call-injected-tool", - name: "lookup", - arguments: '{"query":"weather"}', - }, - }, - { - type: "response.completed", - response: { incomplete_details: null, usage: { input_tokens: 1, output_tokens: 1 } }, - }, - ] - let captured: Record | undefined - let executed: unknown - const executor = Layer.succeed( - RequestExecutor.Service, - RequestExecutor.Service.of({ - execute: (request) => - Effect.gen(function* () { - const web = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie) - captured = (yield* Effect.promise(() => web.json())) as Record - return HttpClientResponse.fromWeb(request, createEventResponse(chunks, true)) - }), }), - ) + { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) }, + ) - await using tmp = await tmpdir({ config: openAIConfig(model, "https://injected-openai.test/v1") }) + it.instance( + "uses injected native request executor for tool calls", + () => + Effect.gen(function* () { + const model = loadFixture("openai", "gpt-5.2").model + const chunks = [ + { + type: "response.output_item.added", + item: { type: "function_call", id: "item-injected-tool", call_id: "call-injected-tool", name: "lookup" }, + }, + { + type: "response.function_call_arguments.delta", + item_id: "item-injected-tool", + delta: '{"query":"weather"}', + }, + { + type: "response.output_item.done", + item: { + type: "function_call", + id: "item-injected-tool", + call_id: "call-injected-tool", + name: "lookup", + arguments: '{"query":"weather"}', + }, + }, + { + type: "response.completed", + response: { incomplete_details: null, usage: { input_tokens: 1, output_tokens: 1 } }, + }, + ] + let captured: Record | undefined + let executed: unknown + const executor = Layer.succeed( + RequestExecutor.Service, + RequestExecutor.Service.of({ + execute: (request) => + Effect.gen(function* () { + const web = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie) + captured = (yield* Effect.promise(() => web.json())) as Record + return HttpClientResponse.fromWeb(request, createEventResponse(chunks, true)) + }), + }), + ) - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native-injected-tool") const agent = { name: "test", @@ -1334,35 +1226,31 @@ describe("session.llm.stream", () => { permission: [{ permission: "*", pattern: "*", action: "allow" }], } satisfies Agent.Info - await drainWith( - llmLayerWithExecutor(executor, { experimentalNativeLlm: true }), - { - user: { - id: MessageID.make("msg_user-native-injected-tool"), - sessionID, - role: "user", - time: { created: Date.now() }, - agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, - } satisfies MessageV2.User, + yield* drainWith(llmLayerWithExecutor(executor, { experimentalNativeLlm: true }), { + user: { + id: MessageID.make("msg_user-native-injected-tool"), sessionID, - model: resolved, - agent, - system: [], - messages: [{ role: "user", content: "Use lookup" }], - 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: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, + } satisfies MessageV2.User, + sessionID, + model: resolved, + agent, + system: [], + messages: [{ role: "user", content: "Use lookup" }], + 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" } + }, + }), }, - ctx, - ) + }) expect(captured?.model).toBe(model.id) expect(captured?.tools).toEqual([ @@ -1380,52 +1268,44 @@ describe("session.llm.stream", () => { }, ]) expect(executed).toEqual({ args: { query: "weather" }, toolCallId: "call-injected-tool" }) - }, - }) - }) + }), + { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, "https://injected-openai.test/v1") }, + ) - test("executes OpenAI tool calls through native runtime", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } + it.instance( + "executes OpenAI tool calls through native runtime", + () => + Effect.gen(function* () { + const model = loadFixture("openai", "gpt-5.2").model + const chunks = [ + { + type: "response.output_item.added", + item: { type: "function_call", id: "item-native-tool", call_id: "call-native-tool", name: "lookup" }, + }, + { + type: "response.function_call_arguments.delta", + item_id: "item-native-tool", + delta: '{"query":"weather"}', + }, + { + type: "response.output_item.done", + item: { + type: "function_call", + id: "item-native-tool", + call_id: "call-native-tool", + name: "lookup", + arguments: '{"query":"weather"}', + }, + }, + { + type: "response.completed", + response: { incomplete_details: null, usage: { input_tokens: 1, output_tokens: 1 } }, + }, + ] + const request = waitRequest("/responses", createEventResponse(chunks, true)) + let executed: unknown - const source = await loadFixture("openai", "gpt-5.5") - const model = source.model - const chunks = [ - { - type: "response.output_item.added", - item: { type: "function_call", id: "item-native-tool", call_id: "call-native-tool", name: "lookup" }, - }, - { - type: "response.function_call_arguments.delta", - item_id: "item-native-tool", - delta: '{"query":"weather"}', - }, - { - type: "response.output_item.done", - item: { - type: "function_call", - id: "item-native-tool", - call_id: "call-native-tool", - name: "lookup", - arguments: '{"query":"weather"}', - }, - }, - { - type: "response.completed", - response: { incomplete_details: null, usage: { input_tokens: 1, output_tokens: 1 } }, - }, - ] - const request = waitRequest("/responses", createEventResponse(chunks, true)) - let executed: unknown - - await using tmp = await tmpdir({ config: openAIConfig(model, `${server.url.origin}/v1`) }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) const sessionID = SessionID.make("session-test-native-tool") const agent = { name: "test", @@ -1434,37 +1314,33 @@ describe("session.llm.stream", () => { permission: [{ permission: "*", pattern: "*", action: "allow" }], } satisfies Agent.Info - await drainWith( - llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), - { - user: { - id: MessageID.make("msg_user-native-tool"), - sessionID, - role: "user", - time: { created: Date.now() }, - agent: agent.name, - model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, - } satisfies MessageV2.User, + yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), { + user: { + id: MessageID.make("msg_user-native-tool"), sessionID, - model: resolved, - agent, - system: [], - messages: [{ role: "user", content: "Use lookup" }], - 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: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, + } satisfies MessageV2.User, + sessionID, + model: resolved, + agent, + system: [], + messages: [{ role: "user", content: "Use lookup" }], + 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" } + }, + }), }, - ctx, - ) + }) - const capture = await request + const capture = yield* Effect.promise(() => request) expect(capture.body.tools).toEqual([ { type: "function", @@ -1480,96 +1356,82 @@ describe("session.llm.stream", () => { }, ]) expect(executed).toEqual({ args: { query: "weather" }, toolCallId: "call-native-tool" }) - }, - }) - }) - - test("accepts user image attachments as data URLs for OpenAI models", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const source = await loadFixture("openai", "gpt-5.5") - const model = source.model - const chunks = [ - { - type: "response.created", - response: { - id: "resp-data-url", - created_at: Math.floor(Date.now() / 1000), - model: model.id, - service_tier: null, - }, - }, - { - type: "response.output_item.added", - output_index: 0, - item: { type: "message", id: "item-data-url", status: "in_progress", role: "assistant", content: [] }, - }, - { - type: "response.content_part.added", - item_id: "item-data-url", - output_index: 0, - content_index: 0, - part: { type: "output_text", text: "", annotations: [] }, - }, - { - type: "response.output_text.delta", - item_id: "item-data-url", - delta: "Looks good", - logprobs: null, - }, - { - type: "response.completed", - response: { - incomplete_details: null, - usage: { - input_tokens: 1, - input_tokens_details: null, - output_tokens: 1, - output_tokens_details: null, - }, - service_tier: null, - }, - }, - ] - const request = waitRequest("/responses", createEventResponse(chunks, true)) - const image = `data:image/png;base64,${Buffer.from( - await Bun.file(path.join(import.meta.dir, "../tool/fixtures/large-image.png")).arrayBuffer(), - ).toString("base64")}` - - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: ["openai"], - provider: { - openai: { - name: "OpenAI", - env: ["OPENAI_API_KEY"], - npm: "@ai-sdk/openai", - api: "https://api.openai.com/v1", - models: { - [model.id]: configModel(model), - }, - options: { - apiKey: "test-openai-key", - baseURL: `${server.url.origin}/v1`, - }, - }, + }), + { + config: () => { + const model = loadFixture("openai", "gpt-5.2").model + return { + enabled_providers: ["openai"], + provider: { + openai: { + name: "OpenAI", + env: ["OPENAI_API_KEY"], + npm: "@ai-sdk/openai", + api: "https://api.openai.com/v1", + models: { [model.id]: JSON.parse(JSON.stringify(model)) as ConfigModel }, + options: { apiKey: "test-openai-key", baseURL: `${state.server!.url.origin}/v1` }, }, - }), - ) + }, + } }, - }) + }, + ) - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx) + it.instance( + "accepts user image attachments as data URLs for OpenAI models", + () => + Effect.gen(function* () { + const model = loadFixture("openai", "gpt-5.2").model + const chunks = [ + { + type: "response.created", + response: { + id: "resp-data-url", + created_at: Math.floor(Date.now() / 1000), + model: model.id, + service_tier: null, + }, + }, + { + type: "response.output_item.added", + output_index: 0, + item: { type: "message", id: "item-data-url", status: "in_progress", role: "assistant", content: [] }, + }, + { + type: "response.content_part.added", + item_id: "item-data-url", + output_index: 0, + content_index: 0, + part: { type: "output_text", text: "", annotations: [] }, + }, + { + type: "response.output_text.delta", + item_id: "item-data-url", + delta: "Looks good", + logprobs: null, + }, + { + type: "response.completed", + response: { + incomplete_details: null, + usage: { + input_tokens: 1, + input_tokens_details: null, + output_tokens: 1, + output_tokens_details: null, + }, + service_tier: null, + }, + }, + ] + const request = waitRequest("/responses", createEventResponse(chunks, true)) + const image = `data:image/png;base64,${Buffer.from( + yield* Effect.promise(() => + Bun.file(path.join(import.meta.dir, "../tool/fixtures/large-image.png")).arrayBuffer(), + ), + ).toString("base64")}` + + const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id)) const sessionID = SessionID.make("session-test-data-url") const agent = { name: "test", @@ -1587,50 +1449,38 @@ describe("session.llm.stream", () => { model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, } satisfies MessageV2.User - await drain( - { - user, - sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Describe this image" }, - { - type: "file", - mediaType: "image/png", - filename: "large-image.png", - data: image, - }, - ], - }, - ] as ModelMessage[], - tools: {}, - }, - ctx, - ) + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Describe this image" }, + { type: "file", mediaType: "image/png", filename: "large-image.png", data: image }, + ], + }, + ] as ModelMessage[], + tools: {}, + }) - const capture = await request + const capture = yield* Effect.promise(() => request) expect(capture.url.pathname.endsWith("/responses")).toBe(true) - }, - }) - }) + }), + { config: () => openAIConfig(loadFixture("openai", "gpt-5.2").model, `${state.server!.url.origin}/v1`) }, + ) - test("sends messages API payload for Anthropic Compatible models", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } + const minimaxFixture = { providerID: "minimax", modelID: "MiniMax-M2.5" } + it.instance( + "sends messages API payload for Anthropic Compatible models", + () => + Effect.gen(function* () { + const model = loadFixture(minimaxFixture.providerID, minimaxFixture.modelID).model - const providerID = "minimax" - const modelID = "MiniMax-M2.5" - const fixture = await loadFixture(providerID, modelID) - const model = fixture.model - - const chunks = [ + const chunks = [ { type: "message_start", message: { @@ -1666,32 +1516,12 @@ describe("session.llm.stream", () => { }, { type: "message_stop" }, ] - const request = waitRequest("/messages", createEventResponse(chunks)) + const request = waitRequest("/messages", createEventResponse(chunks)) - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: [providerID], - provider: { - [providerID]: { - options: { - apiKey: "test-anthropic-key", - baseURL: `${server.url.origin}/v1`, - }, - }, - }, - }), + const resolved = yield* Provider.use.getModel( + ProviderID.make(minimaxFixture.providerID), + ModelID.make(model.id), ) - }, - }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx) const sessionID = SessionID.make("session-test-3") const agent = { name: "test", @@ -1711,20 +1541,17 @@ describe("session.llm.stream", () => { model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") }, } satisfies MessageV2.User - await drain( - { - user, - sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }, - ctx, - ) + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) - const capture = await request + const capture = yield* Effect.promise(() => request) const body = capture.body expect(capture.url.pathname.endsWith("/messages")).toBe(true) @@ -1732,19 +1559,25 @@ describe("session.llm.stream", () => { expect(body.max_tokens).toBe(ProviderTransform.maxOutputTokens(resolved)) expect(body.temperature).toBe(0.4) expect(body.top_p).toBe(0.9) - }, - }) - }) + }), + { + config: () => ({ + enabled_providers: [minimaxFixture.providerID], + provider: { + [minimaxFixture.providerID]: { + options: { apiKey: "test-anthropic-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + }), + }, + ) - test("sends anthropic tool_use blocks with tool_result immediately after them", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const source = await loadFixture("anthropic", "claude-opus-4-6") - const model = source.model - const chunks = [ + it.instance( + "sends anthropic tool_use blocks with tool_result immediately after them", + () => + Effect.gen(function* () { + const model = loadFixture("anthropic", "claude-opus-4-6").model + const chunks = [ { type: "message_start", message: { @@ -1780,39 +1613,9 @@ describe("session.llm.stream", () => { }, { type: "message_stop" }, ] - const request = waitRequest("/messages", createEventResponse(chunks)) + const request = waitRequest("/messages", createEventResponse(chunks)) - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: ["anthropic"], - provider: { - anthropic: { - name: "Anthropic", - env: ["ANTHROPIC_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.anthropic.com/v1", - models: { - [model.id]: configModel(model), - }, - options: { - apiKey: "test-anthropic-key", - baseURL: `${server.url.origin}/v1`, - }, - }, - }, - }), - ) - }, - }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id), ctx) + const resolved = yield* Provider.use.getModel(ProviderID.make("anthropic"), ModelID.make(model.id)) const sessionID = SessionID.make("session-test-anthropic-tools") const agent = { name: "test", @@ -1917,36 +1720,29 @@ describe("session.llm.stream", () => { }, ] as any[] - await drain( - { - user, - sessionID, - model: resolved, - agent, - system: [], - messages: await MessageV2.toModelMessages(input as any, resolved), - tools: { - read: tool({ - description: "Stub read tool", - inputSchema: z.object({ - filePath: z.string(), - }), - execute: async () => ({ output: "stub" }), - }), - glob: tool({ - description: "Stub glob tool", - inputSchema: z.object({ - pattern: z.string(), - path: z.string().optional(), - }), - execute: async () => ({ output: "stub" }), - }), - }, + const modelMessages = yield* Effect.promise(() => MessageV2.toModelMessages(input as any, resolved)) + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: [], + messages: modelMessages, + tools: { + read: tool({ + description: "Stub read tool", + inputSchema: z.object({ filePath: z.string() }), + execute: async () => ({ output: "stub" }), + }), + glob: tool({ + description: "Stub glob tool", + inputSchema: z.object({ pattern: z.string(), path: z.string().optional() }), + execute: async () => ({ output: "stub" }), + }), }, - ctx, - ) + }) - const capture = await request + const capture = yield* Effect.promise(() => request) const body = capture.body expect(capture.url.pathname.endsWith("/messages")).toBe(true) @@ -1977,77 +1773,53 @@ describe("session.llm.stream", () => { expect(messages[toolUseIndex + 1]).toMatchObject({ role: "user", content: [ - { - type: "tool_result", - tool_use_id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", - content: "/root", - }, - { - type: "tool_result", - tool_use_id: "toolu_01APxrADs7VozN8uWzw9WwHr", - content: "No files found", - }, + { type: "tool_result", tool_use_id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", content: "/root" }, + { type: "tool_result", tool_use_id: "toolu_01APxrADs7VozN8uWzw9WwHr", content: "No files found" }, ], }) - }, - }) - }) - - test("sends Google API payload for Gemini models", async () => { - const server = state.server - if (!server) { - throw new Error("Server not initialized") - } - - const providerID = "google" - const modelID = "gemini-2.5-flash" - const fixture = await loadFixture(providerID, modelID) - const model = fixture.model - const pathSuffix = `/v1beta/models/${model.id}:streamGenerateContent` - - const chunks = [ - { - candidates: [ - { - content: { - parts: [{ text: "Hello" }], + }), + { + config: () => { + const model = loadFixture("anthropic", "claude-opus-4-6").model + return { + enabled_providers: ["anthropic"], + provider: { + anthropic: { + name: "Anthropic", + env: ["ANTHROPIC_API_KEY"], + npm: "@ai-sdk/anthropic", + api: "https://api.anthropic.com/v1", + models: { [model.id]: configModel(model) as ConfigModel }, + options: { apiKey: "test-anthropic-key", baseURL: `${state.server!.url.origin}/v1` }, }, - finishReason: "STOP", }, - ], - usageMetadata: { - promptTokenCount: 1, - candidatesTokenCount: 1, - totalTokenCount: 2, - }, + } }, - ] - const request = waitRequest(pathSuffix, createEventResponse(chunks)) + }, + ) - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - enabled_providers: [providerID], - provider: { - [providerID]: { - options: { - apiKey: "test-google-key", - baseURL: `${server.url.origin}/v1beta`, - }, - }, - }, - }), + const geminiFixture = { providerID: "google", modelID: "gemini-2.5-flash" } + it.instance( + "sends Google API payload for Gemini models", + () => + Effect.gen(function* () { + const model = loadFixture(geminiFixture.providerID, geminiFixture.modelID).model + const pathSuffix = `/v1beta/models/${model.id}:streamGenerateContent` + + const chunks = [ + { + candidates: [ + { content: { parts: [{ text: "Hello" }] }, finishReason: "STOP" }, + ], + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }, + }, + ] + const request = waitRequest(pathSuffix, createEventResponse(chunks)) + + const resolved = yield* Provider.use.getModel( + ProviderID.make(geminiFixture.providerID), + ModelID.make(model.id), ) - }, - }) - - await withTestInstance({ - directory: tmp.path, - fn: async (ctx) => { - const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx) const sessionID = SessionID.make("session-test-4") const agent = { name: "test", @@ -2064,23 +1836,20 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, + model: { providerID: ProviderID.make(geminiFixture.providerID), modelID: resolved.id }, } satisfies MessageV2.User - await drain( - { - user, - sessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }, - ctx, - ) + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) - const capture = await request + const capture = yield* Effect.promise(() => request) const body = capture.body const config = body.generationConfig as | { temperature?: number; topP?: number; maxOutputTokens?: number } @@ -2090,7 +1859,16 @@ describe("session.llm.stream", () => { expect(config?.temperature).toBe(0.3) expect(config?.topP).toBe(0.8) expect(config?.maxOutputTokens).toBe(ProviderTransform.maxOutputTokens(resolved)) - }, - }) - }) + }), + { + config: () => ({ + enabled_providers: [geminiFixture.providerID], + provider: { + [geminiFixture.providerID]: { + options: { apiKey: "test-google-key", baseURL: `${state.server!.url.origin}/v1beta` }, + }, + }, + }), + }, + ) })