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
8 changes: 6 additions & 2 deletions packages/ai/src/providers/opencode-go.ts
Original file line number Diff line number Diff line change
@@ -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(),
},
Expand Down
50 changes: 50 additions & 0 deletions packages/ai/test/opencode-go.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof fetch>(
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");
});
2 changes: 1 addition & 1 deletion packages/ai/test/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe("builtin providers", () => {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 0,
cacheWrite: 3,
});
}
});
Expand Down
39 changes: 21 additions & 18 deletions packages/ai/test/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions packages/ai/test/supports-xhigh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
Expand Down
2 changes: 1 addition & 1 deletion scripts/runtime-identity-allowlist.json
Original file line number Diff line number Diff line change
@@ -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"
],
Expand Down
Loading