diff --git a/packages/ai/src/providers/opencode-go.ts b/packages/ai/src/providers/opencode-go.ts index 4f5452c16..a933908f3 100644 --- a/packages/ai/src/providers/opencode-go.ts +++ b/packages/ai/src/providers/opencode-go.ts @@ -1,18 +1,22 @@ import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts"; import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; import { envApiKeyAuth } from "../auth/helpers.ts"; import { createProvider, type Provider } from "../models.ts"; import { OPENCODE_GO_MODELS } from "./opencode-go.models.ts"; -export function opencodeGoProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> { - return createProvider<"anthropic-messages" | "openai-completions" | "openai-responses">({ +export function opencodeGoProvider(): Provider< + "anthropic-messages" | "google-generative-ai" | "openai-completions" | "openai-responses" +> { + return createProvider({ id: "opencode-go", name: "OpenCode Go", auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) }, models: Object.values(OPENCODE_GO_MODELS), api: { "anthropic-messages": anthropicMessagesApi(), + "google-generative-ai": googleGenerativeAIApi(), "openai-completions": openAICompletionsApi(), "openai-responses": openAIResponsesApi(), }, diff --git a/packages/ai/test/opencode-go.test.ts b/packages/ai/test/opencode-go.test.ts new file mode 100644 index 000000000..4edbc11ea --- /dev/null +++ b/packages/ai/test/opencode-go.test.ts @@ -0,0 +1,50 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { createModels } from "../src/models.ts"; +import { opencodeGoProvider } from "../src/providers/opencode-go.ts"; +import type { Model } from "../src/types.ts"; + +afterEach(() => vi.unstubAllGlobals()); + +it("streams Google models through OpenCode Go with its API key and endpoint", async () => { + const fetchMock = vi.fn( + async () => + new Response( + `data: ${JSON.stringify({ + candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }], + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }, + })}\n\n`, + { headers: { "Content-Type": "text/event-stream" } }, + ), + ); + vi.stubGlobal("fetch", fetchMock); + const models = createModels({ + authContext: { + env: async (name) => (name === "OPENCODE_API_KEY" ? "test-opencode-key" : undefined), + fileExists: async () => false, + }, + }); + models.setProvider(opencodeGoProvider()); + const model: Model<"google-generative-ai"> = { + id: "gemini-test", + name: "Gemini test", + api: "google-generative-ai", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8192, + maxTokens: 1024, + }; + const result = await models.completeSimple(model, { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + }); + expect(result.errorMessage).toBeUndefined(); + expect(result.stopReason).toBe("stop"); + expect(result.content).toEqual([expect.objectContaining({ type: "text", text: "ok" })]); + expect(fetchMock).toHaveBeenCalledOnce(); + const [input, init] = fetchMock.mock.calls[0]; + const request = new Request(input, init); + expect(request.url).toBe("https://opencode.ai/zen/go/v1/models/gemini-test:streamGenerateContent?alt=sse"); + expect(request.headers.get("x-goog-api-key")).toBe("test-opencode-key"); +}); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 14f0b20a0..2d1644bf7 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -73,7 +73,7 @@ describe("builtin providers", () => { input: 3, output: 15, cacheRead: 0.3, - cacheWrite: 0, + cacheWrite: 3, }); } }); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index b97014a94..cea914d26 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -811,29 +811,32 @@ describe("Generate E2E Tests", () => { }); }); - describe.skipIf(!process.env.NVIDIA_API_KEY)("NVIDIA NIM Provider (Nemotron 3 Super via OpenAI Completions)", () => { - const llm = getModel("nvidia", "nvidia/nemotron-3-super-120b-a12b"); + describe.skipIf(!process.env.NVIDIA_API_KEY)( + "NVIDIA NIM Provider (Nemotron 3.5 Super VL via OpenAI Completions)", + () => { + const llm = getModel("nvidia", "nvidia/nemotron-3.5-super-vl-120b-a12b"); - it("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm); - }); + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); - it("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm); - }); + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); - it("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm); - }); + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); - it("should handle thinking mode", { retry: 3 }, async () => { - await handleThinking(llm, { reasoningEffort: "high" }); - }); + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, { reasoningEffort: "high" }); + }); - it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { - await multiTurn(llm, { reasoningEffort: "high" }); - }); - }); + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, { reasoningEffort: "high" }); + }); + }, + ); describe.skipIf(!process.env.OPENROUTER_API_KEY)("OpenRouter Provider (glm-4.5v via OpenAI Completions)", () => { const llm = getModel("openrouter", "z-ai/glm-4.5v"); diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index 490e50a5d..f6db09a8a 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -88,8 +88,8 @@ describe("getSupportedThinkingLevels", () => { expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "max"]); }); - it("includes only high/max plus off for DeepSeek V4 Flash on opencode-go", () => { - const model = getModel("opencode-go", "deepseek-v4-flash"); + it("includes only high/max plus off for DeepSeek V4 Pro on opencode-go", () => { + const model = getModel("opencode-go", "deepseek-v4-pro"); expect(model).toBeDefined(); expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high", "max"]); }); diff --git a/scripts/runtime-identity-allowlist.json b/scripts/runtime-identity-allowlist.json index e11489b31..ce22be665 100644 --- a/scripts/runtime-identity-allowlist.json +++ b/scripts/runtime-identity-allowlist.json @@ -1,7 +1,7 @@ [ { "path": "README.md", - "context": "Diffex is a fork of the [Pi Agent Harness](https://github.com/earendil-works/pi).", + "context": "Diffex builds on the [Pi Agent Harness](https://github.com/earendil-works/pi) to extend its capabilities.", "identities": [ "earendil-works" ],