From 33ef3b01f8cfbbc723911147f99986b7e4817d72 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 26 Apr 2026 20:41:32 -0400 Subject: [PATCH] test(opencode): cover native Gemini parity --- packages/opencode/src/provider/llm-bridge.ts | 7 +- packages/opencode/src/session/llm-native.ts | 2 +- .../opencode/test/provider/llm-bridge.test.ts | 22 +++++- .../opencode/test/session/llm-native.test.ts | 73 ++++++++++++++++++- 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/llm-bridge.ts b/packages/opencode/src/provider/llm-bridge.ts index 8455cd4c79..f75c79044d 100644 --- a/packages/opencode/src/provider/llm-bridge.ts +++ b/packages/opencode/src/provider/llm-bridge.ts @@ -1,4 +1,5 @@ import { + AmazonBedrock, Anthropic, GitHubCopilot, Google, @@ -23,6 +24,7 @@ type Input = { } const PROVIDERS: Record = { + "@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), diff --git a/packages/opencode/src/session/llm-native.ts b/packages/opencode/src/session/llm-native.ts index 2d7b990c44..41a53a9177 100644 --- a/packages/opencode/src/session/llm-native.ts +++ b/packages/opencode/src/session/llm-native.ts @@ -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, }, diff --git a/packages/opencode/test/provider/llm-bridge.test.ts b/packages/opencode/test/provider/llm-bridge.test.ts index 5d68c21b4c..5177f217db 100644 --- a/packages/opencode/test/provider/llm-bridge.test.ts +++ b/packages/opencode/test/provider/llm-bridge.test.ts @@ -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]) }) }) diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 2b03eff76a..3530b6541d 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -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 }, + }) + })) })