Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" &&
Expand Down
88 changes: 88 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2057,3 +2057,91 @@ 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<Record<string, unknown>> }
const server = yield* Effect.acquireRelease(
Effect.sync(() =>
Bun.serve({
port: 0,
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 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)
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<void> }).closeImageFiles())
expect(receipt.deletes).toBe(1)
}),
)
Loading