Skip to content

Commit 20bed21

Browse files
authored
Merge pull request #170 from browser-use/screenshot-default-wrap
fix(provider): wrap default OpenAI screenshot selector
2 parents fa8b091 + d752e3a commit 20bed21

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

‎packages/opencode/src/provider/provider.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,6 +1789,18 @@ const layer = Layer.effect(
17891789
s.imageFiles.add(wrapped.closeImageFiles)
17901790
return wrapped
17911791
}
1792+
// Config-only providers can select the SDK default without a custom loader.
1793+
// OpenAI defaults to Responses; wrap that selector as well.
1794+
const languageModel = loaded.languageModel.bind(loaded)
1795+
loaded.languageModel = (id: string) => {
1796+
const wrapped = withImageFiles(languageModel(id), {
1797+
baseURL,
1798+
apiKey: options.apiKey,
1799+
fetch: options.fetch,
1800+
})
1801+
s.imageFiles.add(wrapped.closeImageFiles)
1802+
return wrapped
1803+
}
17921804
}
17931805
if (
17941806
model.api.npm === "@ai-sdk/anthropic" &&

‎packages/opencode/test/provider/provider.test.ts‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,3 +2057,91 @@ it.effect("opencode loader keeps paid models when auth exists", () =>
20572057
expect(keyedCount).toBeGreaterThan(0)
20582058
}).pipe(provideMultiInstance),
20592059
)
2060+
2061+
it.instance(
2062+
"configuration-only OpenAI gateway wraps the selected Responses model",
2063+
Effect.gen(function* () {
2064+
yield* remove("OPENAI_API_KEY")
2065+
const receipt = { uploads: 0, deletes: 0, bodies: [] as Array<Record<string, unknown>> }
2066+
const server = yield* Effect.acquireRelease(
2067+
Effect.sync(() =>
2068+
Bun.serve({
2069+
port: 0,
2070+
async fetch(request) {
2071+
const url = new URL(request.url)
2072+
if (request.method === "DELETE") {
2073+
receipt.deletes++
2074+
return Response.json({ deleted: true })
2075+
}
2076+
if (url.pathname.endsWith("/capability"))
2077+
return Response.json({ supported: true, scope: "owned-default-selector" })
2078+
if (url.pathname.endsWith("/image-files")) {
2079+
receipt.uploads++
2080+
return Response.json({ file_id: "file-owned", signature: "proof", expires_at: Date.now() / 1000 + 3600 })
2081+
}
2082+
receipt.bodies.push(await request.json())
2083+
return Response.json({
2084+
id: "resp_owned",
2085+
object: "response",
2086+
created_at: 1,
2087+
model: "gateway-fixture",
2088+
output: [],
2089+
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
2090+
status: "completed",
2091+
})
2092+
},
2093+
}),
2094+
),
2095+
(server) => Effect.sync(() => server.stop(true)),
2096+
)
2097+
const instance = yield* TestInstance
2098+
yield* Effect.promise(() =>
2099+
Bun.write(
2100+
path.join(instance.directory, "bcode.json"),
2101+
JSON.stringify({
2102+
provider: {
2103+
openai: {
2104+
npm: "@ai-sdk/openai",
2105+
options: { apiKey: "v4rt_owned", baseURL: `${server.url}api/v4/llm/openai/v1` },
2106+
models: { "gateway-fixture": { name: "Gateway Fixture", limit: { context: 128000, output: 4096 } } },
2107+
},
2108+
},
2109+
}),
2110+
),
2111+
)
2112+
const provider = yield* Provider.Service
2113+
const model = yield* provider.getModel(ProviderV2.ID.openai, ModelV2.ID.make("gateway-fixture"))
2114+
const language = yield* provider.getLanguage(model)
2115+
expect(typeof (language as unknown as { closeImageFiles?: unknown }).closeImageFiles).toBe("function")
2116+
const screenshot = Buffer.from("owned screenshot fixture").toString("base64")
2117+
for (const turn of [1, 2, 3])
2118+
yield* Effect.promise(() =>
2119+
language.doGenerate({
2120+
prompt: [
2121+
{
2122+
role: "user",
2123+
content: [
2124+
{ type: "text", text: `turn ${turn}` },
2125+
{
2126+
type: "file",
2127+
mediaType: "image/png",
2128+
data: screenshot,
2129+
providerOptions: { openai: { imageDetail: "high" } },
2130+
},
2131+
],
2132+
},
2133+
],
2134+
}),
2135+
)
2136+
expect(receipt.uploads).toBe(1)
2137+
expect(receipt.bodies).toHaveLength(3)
2138+
for (const body of receipt.bodies) {
2139+
const serialized = JSON.stringify(body)
2140+
expect(serialized).toContain("file-owned")
2141+
expect(serialized).not.toContain(screenshot)
2142+
expect(serialized).toContain("high")
2143+
}
2144+
yield* Effect.promise(() => (language as unknown as { closeImageFiles(): Promise<void> }).closeImageFiles())
2145+
expect(receipt.deletes).toBe(1)
2146+
}),
2147+
)

0 commit comments

Comments
 (0)