test(opencode): cover native Gemini parity

This commit is contained in:
Kit Langton
2026-04-26 20:41:32 -04:00
parent a26f2c905f
commit 33ef3b01f8
4 changed files with 99 additions and 5 deletions
+6 -1
View File
@@ -1,4 +1,5 @@
import {
AmazonBedrock,
Anthropic,
GitHubCopilot,
Google,
@@ -23,6 +24,7 @@ type Input = {
}
const PROVIDERS: Record<string, ProviderDefinition> = {
"@ai-sdk/amazon-bedrock": AmazonBedrock.provider,
"@ai-sdk/anthropic": Anthropic.provider,
"@ai-sdk/baseten": OpenAICompatibleFamily.provider,
"@ai-sdk/cerebras": OpenAICompatibleFamily.provider,
@@ -102,8 +104,11 @@ const capabilities = (input: Input, selected: Protocol) =>
streamingInput: selected !== "gemini" && input.model.capabilities.toolcall,
},
cache: {
// Both Anthropic Messages and Bedrock Converse honour positional cache
// markers — Anthropic via `cache_control` on content blocks, Bedrock via
// its `cachePoint` marker block (added to BedrockConverse in 9d7d518ac).
prompt: ["anthropic-messages", "bedrock-converse"].includes(selected),
contentBlocks: selected === "anthropic-messages",
contentBlocks: ["anthropic-messages", "bedrock-converse"].includes(selected),
},
reasoning: {
efforts: reasoningEfforts(input),
+1 -1
View File
@@ -163,7 +163,7 @@ export const toolDefinition = (input: { readonly model: Provider.Model; readonly
LLM.tool({
name: input.tool.id,
description: input.tool.description,
inputSchema: Object.fromEntries(Object.entries(EffectZod.toJsonSchema(input.tool.parameters))),
inputSchema: EffectZod.toJsonSchema(input.tool.parameters),
native: {
opencodeToolID: input.tool.id,
},
@@ -148,11 +148,29 @@ describe("ProviderLLMBridge", () => {
})
})
test("maps Amazon Bedrock to Converse with bearer auth and content-block cache", () => {
const ref = ProviderLLMBridge.toModelRef({
provider: provider({ id: ProviderID.make("amazon-bedrock"), key: "bedrock-bearer-key" }),
model: model({
id: "anthropic.claude-3-5-sonnet-20240620-v1:0",
providerID: "amazon-bedrock",
npm: "@ai-sdk/amazon-bedrock",
}),
})
expect(ref).toMatchObject({
protocol: "bedrock-converse",
headers: { authorization: "Bearer bedrock-bearer-key" },
})
// Bedrock Converse supports both prompt-level and positional content-block
// cache markers (cachePoint blocks landed in 9d7d518ac).
expect(ref?.capabilities.cache).toMatchObject({ prompt: true, contentBlocks: true })
})
test("leaves undecided provider packages unmapped", () => {
const unsupported = [
["mistral", "mistral-large", "@ai-sdk/mistral"],
["azure", "gpt-4.1", "@ai-sdk/azure"],
["amazon-bedrock", "anthropic.claude-3-5-sonnet-20240620-v1:0", "@ai-sdk/amazon-bedrock"],
] as const
expect(
@@ -162,6 +180,6 @@ describe("ProviderLLMBridge", () => {
model: model({ id: modelID, providerID, npm }),
}),
),
).toEqual([undefined, undefined, undefined])
).toEqual([undefined, undefined])
})
})
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { AnthropicMessages, OpenAICompatibleChat } from "@opencode-ai/llm"
import { AnthropicMessages, Gemini, OpenAICompatibleChat } from "@opencode-ai/llm"
import { client } from "@opencode-ai/llm/adapter"
import { OpenAIResponses } from "@opencode-ai/llm/provider/openai-responses"
import { Cause, Effect, Exit, Layer, Schema } from "effect"
@@ -506,4 +506,75 @@ describe("LLMNative.request", () => {
temperature: 0,
})
}))
it.effect("prepares Gemini text and tool request body", () => Effect.gen(function* () {
const mdl = model({
id: ModelID.make("gemini-2.5-flash"),
providerID: ProviderID.make("google"),
api: { id: "gemini-2.5-flash", url: "https://generativelanguage.googleapis.com/v1beta", npm: "@ai-sdk/google" },
})
const userID = MessageID.ascending()
const assistantID = MessageID.ascending()
const request = yield* LLMNative.request({
provider: ProviderTest.info({ id: ProviderID.make("google"), key: "google-key" }, mdl),
model: mdl,
system: ["You are concise."],
generation: { maxTokens: 32, temperature: 0 },
messages: [
userMessage(mdl, userID, [textPart(userID, "What is the weather?")]),
assistantMessage(mdl, assistantID, userID, [
toolPart(assistantID, {
callID: "call_1",
tool: "lookup",
state: {
status: "completed",
input: { query: "weather" },
output: '{"forecast":"sunny"}',
title: "Weather",
metadata: {},
time: { start: 1, end: 2 },
},
}),
]),
],
tools: [lookupTool],
toolChoice: "lookup",
})
const prepared = yield* client({ adapters: [Gemini.adapter] }).prepare(request)
expect(request.model).toMatchObject({
provider: "google",
protocol: "gemini",
baseURL: "https://generativelanguage.googleapis.com/v1beta",
headers: { "x-goog-api-key": "google-key" },
})
expect(prepared.target).toMatchObject({
systemInstruction: { parts: [{ text: "You are concise." }] },
contents: [
{ role: "user", parts: [{ text: "What is the weather?" }] },
{ role: "model", parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }] },
{
role: "user",
parts: [{ functionResponse: { name: "lookup", response: { name: "lookup", content: '{"forecast":"sunny"}' } } }],
},
],
tools: [
{
functionDeclarations: [
{
name: "lookup",
description: "Lookup project data",
parameters: {
type: "object",
properties: { query: { type: "string", description: "Search query" } },
required: ["query"],
},
},
],
},
],
toolConfig: { functionCallingConfig: { mode: "ANY", allowedFunctionNames: ["lookup"] } },
generationConfig: { maxOutputTokens: 32, temperature: 0 },
})
}))
})