From d100ce1f66c0b69eda0492ed0e749347665589a5 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Fri, 2 Oct 2026 05:35:31 +0000 Subject: [PATCH 1/2] fix(provider): wrap default OpenAI screenshot selector --- packages/opencode/src/provider/provider.ts | 12 +++ .../opencode/test/provider/provider.test.ts | 88 ++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ff4a6a9d62..a7c719da06 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1789,6 +1789,18 @@ const layer = Layer.effect( s.imageFiles.add(wrapped.closeImageFiles) return wrapped } + // Config-only providers can select the SDK default without a custom loader. + // OpenAI defaults to Responses; wrap that selector as well. + const languageModel = loaded.languageModel.bind(loaded) + loaded.languageModel = (id: string) => { + const wrapped = withImageFiles(languageModel(id), { + baseURL, + apiKey: options.apiKey, + fetch: options.fetch, + }) + s.imageFiles.add(wrapped.closeImageFiles) + return wrapped + } } if ( model.api.npm === "@ai-sdk/anthropic" && diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 058bad23a1..1379401bf0 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1,5 +1,5 @@ import { afterEach, expect, test } from "bun:test" -import { mkdir, unlink } from "fs/promises" +import { mkdir, unlink, rename } from "fs/promises" import path from "path" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -2057,3 +2057,89 @@ it.effect("opencode loader keeps paid models when auth exists", () => expect(keyedCount).toBeGreaterThan(0) }).pipe(provideMultiInstance), ) + +it.instance( + "configuration-only OpenAI gateway wraps the selected Responses model", + Effect.gen(function* () { + yield* remove("OPENAI_API_KEY") + const receipt = { uploads: 0, deletes: 0, bodies: [] as Array> } + yield* Effect.acquireRelease( + Effect.sync(() => + Bun.serve({ + port: 19091, + async fetch(request) { + const url = new URL(request.url) + if (request.method === "DELETE") { + receipt.deletes++ + return Response.json({ deleted: true }) + } + if (url.pathname.endsWith("/capability")) + return Response.json({ supported: true, scope: "owned-default-selector" }) + if (url.pathname.endsWith("/image-files")) { + receipt.uploads++ + return Response.json({ file_id: "file-owned", signature: "proof", expires_at: Date.now() / 1000 + 3600 }) + } + receipt.bodies.push(await request.json()) + return Response.json({ + id: "resp_owned", + object: "response", + created_at: 1, + model: "gateway-fixture", + output: [], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + status: "completed", + }) + }, + }), + ), + (server) => Effect.sync(() => server.stop(true)), + ) + const provider = yield* Provider.Service + const model = yield* provider.getModel(ProviderV2.ID.openai, ModelV2.ID.make("gateway-fixture")) + const language = yield* provider.getLanguage(model) + expect(typeof (language as unknown as { closeImageFiles?: unknown }).closeImageFiles).toBe("function") + const screenshot = Buffer.from("owned screenshot fixture").toString("base64") + for (const turn of [1, 2, 3]) + yield* Effect.promise(() => + language.doGenerate({ + prompt: [ + { + role: "user", + content: [ + { type: "text", text: `turn ${turn}` }, + { + type: "file", + mediaType: "image/png", + data: screenshot, + providerOptions: { openai: { imageDetail: "high" } }, + }, + ], + }, + ], + }), + ) + expect(receipt.uploads).toBe(1) + expect(receipt.bodies).toHaveLength(3) + for (const body of receipt.bodies) { + const serialized = JSON.stringify(body) + expect(serialized).toContain("file-owned") + expect(serialized).not.toContain(screenshot) + expect(serialized).toContain("high") + } + yield* Effect.promise(() => (language as unknown as { closeImageFiles(): Promise }).closeImageFiles()) + expect(receipt.deletes).toBe(1) + }), + { + init: (directory) => + Effect.promise(() => rename(path.join(directory, "opencode.json"), path.join(directory, "bcode.json"))), + config: { + provider: { + openai: { + npm: "@ai-sdk/openai", + options: { apiKey: "v4rt_owned", baseURL: "http://127.0.0.1:19091/api/v4/llm/openai/v1" }, + models: { "gateway-fixture": { name: "Gateway Fixture", limit: { context: 128000, output: 4096 } } }, + }, + }, + }, + }, +) From d752e3a9b3ce4a88ae03e8ce3afa81f5d3c4ef45 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Fri, 2 Oct 2026 05:43:12 +0000 Subject: [PATCH 2/2] test(provider): isolate screenshot fixture port --- .../opencode/test/provider/provider.test.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 1379401bf0..c6c8fb20bb 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1,5 +1,5 @@ import { afterEach, expect, test } from "bun:test" -import { mkdir, unlink, rename } from "fs/promises" +import { mkdir, unlink } from "fs/promises" import path from "path" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -2063,10 +2063,10 @@ it.instance( Effect.gen(function* () { yield* remove("OPENAI_API_KEY") const receipt = { uploads: 0, deletes: 0, bodies: [] as Array> } - yield* Effect.acquireRelease( + const server = yield* Effect.acquireRelease( Effect.sync(() => Bun.serve({ - port: 19091, + port: 0, async fetch(request) { const url = new URL(request.url) if (request.method === "DELETE") { @@ -2094,6 +2094,21 @@ it.instance( ), (server) => Effect.sync(() => server.stop(true)), ) + const instance = yield* TestInstance + yield* Effect.promise(() => + Bun.write( + path.join(instance.directory, "bcode.json"), + JSON.stringify({ + provider: { + openai: { + npm: "@ai-sdk/openai", + options: { apiKey: "v4rt_owned", baseURL: `${server.url}api/v4/llm/openai/v1` }, + models: { "gateway-fixture": { name: "Gateway Fixture", limit: { context: 128000, output: 4096 } } }, + }, + }, + }), + ), + ) const provider = yield* Provider.Service const model = yield* provider.getModel(ProviderV2.ID.openai, ModelV2.ID.make("gateway-fixture")) const language = yield* provider.getLanguage(model) @@ -2129,17 +2144,4 @@ it.instance( yield* Effect.promise(() => (language as unknown as { closeImageFiles(): Promise }).closeImageFiles()) expect(receipt.deletes).toBe(1) }), - { - init: (directory) => - Effect.promise(() => rename(path.join(directory, "opencode.json"), path.join(directory, "bcode.json"))), - config: { - provider: { - openai: { - npm: "@ai-sdk/openai", - options: { apiKey: "v4rt_owned", baseURL: "http://127.0.0.1:19091/api/v4/llm/openai/v1" }, - models: { "gateway-fixture": { name: "Gateway Fixture", limit: { context: 128000, output: 4096 } } }, - }, - }, - }, - }, )