Files
openclaw/extensions/opencode/index.test.ts
Peter Steinberger 63d9c69c11 fix(opencode): discover Zen and Go models on demand (#129831)
* fix(opencode): discover provider models on demand

* fix(opencode): honor provider-scoped catalog discovery

* fix(opencode): scope Go models to authenticated catalogs

* test(opencode): verify dynamic Go models efficiently
2026-08-25 23:38:11 -07:00

647 lines
23 KiB
TypeScript

import { readFileSync } from "node:fs";
import {
registerProviderPlugin,
registerSingleProviderPlugin,
requireRegisteredProvider,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
import { expectPassthroughReplayPolicy } from "openclaw/plugin-sdk/provider-test-contracts";
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import plugin from "./index.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
buildOpencodeZenLiveProviderConfig,
prepareOpencodeZenModel,
resolveOpencodeZenStarterModel,
} from "./provider-catalog.js";
const requireRecord = createRequireRecord("record", "expected-label-record");
const METADATA_URL = "https://models.opencode.ai/api.json";
const MODELS_URL = "https://opencode.ai/zen/v1/models";
const OFFLINE_MODEL_IDS = [
"claude-opus-5",
"gpt-5.6-sol",
"gpt-5-nano",
"gemini-3.6-flash",
"minimax-m3",
"kimi-k3",
"big-pickle",
] as const;
type UpstreamFixtureModel = {
id: string;
name?: string;
npm?: string;
status?: string;
reasoningEfforts?: string[];
};
function upstreamCatalog(models: readonly UpstreamFixtureModel[]) {
return {
opencode: {
id: "opencode",
api: "https://opencode.ai/zen/v1",
npm: "@ai-sdk/openai-compatible",
models: Object.fromEntries(
models.map((model) => [
model.id,
{
id: model.id,
name: model.name ?? model.id,
reasoning: true,
tool_call: true,
modalities: { input: ["text", "image"], output: ["text"] },
limit: { context: 1_000_000, input: 900_000, output: 131_072 },
cost: { input: 0, output: 0, cache_read: 0 },
...(model.npm ? { provider: { npm: model.npm } } : {}),
...(model.status ? { status: model.status } : {}),
...(model.reasoningEfforts
? { reasoning_options: [{ type: "effort", values: model.reasoningEfforts }] }
: {}),
},
]),
),
},
};
}
function guardedResponse(url: string, body: unknown) {
return {
response: new Response(JSON.stringify(body)),
finalUrl: url,
release: vi.fn(async () => undefined),
};
}
function createCatalogFetchGuard(params: {
metadata: readonly UpstreamFixtureModel[];
advertised: readonly string[] | ((authorization: string | null) => readonly string[]);
}) {
return vi.fn(async ({ url, init }: { url: string; init?: RequestInit }) => {
if (url === METADATA_URL) {
expect(new Headers(init?.headers).has("authorization")).toBe(false);
return guardedResponse(url, upstreamCatalog(params.metadata));
}
if (url === MODELS_URL) {
const authorization = new Headers(init?.headers).get("authorization");
const ids =
typeof params.advertised === "function"
? params.advertised(authorization)
: params.advertised;
return guardedResponse(url, { data: ids.map((id) => ({ id, object: "model" })) });
}
throw new Error(`unexpected discovery URL: ${url}`);
});
}
function expectSeedModels(models: ReadonlyArray<{ id: string }>) {
expect(models.map((model) => model.id)).toEqual(OFFLINE_MODEL_IDS);
}
describe("opencode provider plugin", () => {
beforeEach(() => {
clearLiveCatalogCacheForTests();
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it("registers only the Zen auth choice from its own provider manifest", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(provider.id).toBe("opencode");
expect(provider.envVars).toEqual(["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]);
expect(provider.auth.map((method) => method.wizard?.choiceId)).toEqual(["opencode-zen"]);
expect(provider.auth[0]?.wizard).toMatchObject({
choiceLabel: "OpenCode Zen catalog",
groupId: "opencode",
groupHint: "Shared API key infrastructure for Zen + Go",
});
});
it("registers image media understanding through the OpenCode plugin", async () => {
const { mediaProviders } = await registerProviderPlugin({
plugin,
id: "opencode",
name: "OpenCode Zen Provider",
});
const mediaProvider = mediaProviders.find((provider) => provider.id === "opencode");
expect(mediaProvider?.capabilities).toEqual(["image"]);
expect(mediaProvider?.defaultModels).toEqual({ image: "gpt-5-nano" });
expect(typeof mediaProvider?.describeImage).toBe("function");
});
it("owns Gemini-only passthrough replay policy", async () => {
await expectPassthroughReplayPolicy({
plugin,
providerId: "opencode",
modelId: "gemini-2.5-pro",
sanitizeThoughtSignatures: true,
});
await expectPassthroughReplayPolicy({
plugin,
providerId: "opencode",
modelId: "claude-opus-4.6",
});
});
it("keeps the offline manifest seed resolvable without downloading metadata", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new Error("unexpected fetch"));
const provider = await registerSingleProviderPlugin(plugin);
const result = await provider.staticCatalog?.run({} as never);
if (!result || !("provider" in result)) {
throw new Error("expected registered OpenCode Zen static provider");
}
expectSeedModels(result.provider.models);
expectSeedModels(manifest.modelCatalog.providers.opencode.models);
for (const modelId of OFFLINE_MODEL_IDS) {
expect(provider.resolveDynamicModel?.({ modelId } as never)).toMatchObject({ id: modelId });
}
expect(
provider.resolveDynamicModel?.({ modelId: "unknown-offline-fixture" } as never),
).toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it("retains the documented offline provider families", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const docs = readFileSync("docs/providers/opencode.md", "utf8");
const exampleRow = docs.match(/^\| Example models\s+\| (?<examples>.+) \|$/m);
if (!exampleRow?.groups?.examples) {
throw new Error("expected OpenCode Zen example model row");
}
const examples = new Set(
[...exampleRow.groups.examples.matchAll(/`opencode\/(.*?)`/g)].map((match) => match[1]),
);
for (const modelId of OFFLINE_MODEL_IDS.filter((id) => examples.has(id))) {
expect(provider.resolveDynamicModel?.({ modelId } as never)).toMatchObject({ id: modelId });
}
expect(provider.resolveDynamicModel?.({ modelId: "claude-opus-5" } as never)).toMatchObject({
api: "anthropic-messages",
baseUrl: "https://opencode.ai/zen",
});
expect(provider.resolveDynamicModel?.({ modelId: "gpt-5.6-sol" } as never)).toMatchObject({
api: "openai-responses",
baseUrl: "https://opencode.ai/zen/v1",
});
expect(provider.resolveDynamicModel?.({ modelId: "gemini-3.6-flash" } as never)).toMatchObject({
api: "google-generative-ai",
baseUrl: "https://opencode.ai/zen/v1",
});
});
it("exposes the offline seed through provider discovery without network access", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new Error("unexpected fetch"));
const { default: discovery } = await import("./provider-discovery.js");
const result = await discovery.staticCatalog?.run({} as never);
if (!result || !("provider" in result)) {
throw new Error("expected OpenCode Zen static provider");
}
expectSeedModels(result.provider.models);
expect(manifest.modelCatalog.discovery.opencode).toBe("runtime");
expect(fetchMock).not.toHaveBeenCalled();
});
it("never downloads metadata when OpenCode has no configured credentials", async () => {
vi.stubEnv("OPENCODE_API_KEY", undefined);
vi.stubEnv("OPENCODE_ZEN_API_KEY", undefined);
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new Error("unexpected fetch"));
const provider = await registerSingleProviderPlugin(plugin);
await expect(
provider.catalog?.run({
config: {},
env: {},
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({ apiKey: undefined, mode: "none", source: "none" }),
} as never),
).resolves.toBeNull();
await expect(
provider.prepareDynamicModel?.({
config: {},
provider: "opencode",
modelId: "unconfigured-preview-fixture",
modelRegistry: {},
} as never),
).resolves.toBeUndefined();
await expect(
provider.prepareDynamicModel?.({
config: {},
provider: "opencode",
modelId: "unconfigured-preview-fixture",
modelRegistry: {},
authProfileMode: "api_key",
authProfileId: "anthropic:default",
} as never),
).resolves.toBeUndefined();
expectSeedModels((await buildOpencodeZenLiveProviderConfig()).models);
expect(fetchMock).not.toHaveBeenCalled();
});
it("does not resolve OpenCode credentials or download metadata for another provider scope", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new Error("unexpected fetch"));
const resolveProviderApiKey = vi.fn(
(): { apiKey: string; discoveryApiKey: string | undefined } => ({
apiKey: "configured-opencode-key",
discoveryApiKey: "configured-opencode-key",
}),
);
const provider = await registerSingleProviderPlugin(plugin);
await expect(
provider.catalog?.run({
config: {},
env: {},
providerIds: ["anthropic"],
resolveProviderApiKey,
} as never),
).resolves.toBeNull();
expect(resolveProviderApiKey).not.toHaveBeenCalled();
expect(fetchMock).not.toHaveBeenCalled();
resolveProviderApiKey.mockReturnValueOnce({
apiKey: NON_ENV_SECRETREF_MARKER,
discoveryApiKey: undefined,
});
const ownScope = await provider.catalog?.run({
config: {},
env: {},
providerIds: ["opencode"],
resolveProviderApiKey,
} as never);
expect(ownScope).toMatchObject({ provider: { apiKey: NON_ENV_SECRETREF_MARKER } });
expect(resolveProviderApiKey).toHaveBeenCalledOnce();
expect(fetchMock).not.toHaveBeenCalled();
});
it("downloads upstream metadata only after an explicitly selected provider is configured", async () => {
vi.stubEnv("OPENCODE_API_KEY", undefined);
vi.stubEnv("OPENCODE_ZEN_API_KEY", undefined);
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(
new Response(
JSON.stringify(
upstreamCatalog([{ id: "configured-selection-fixture", npm: "@ai-sdk/anthropic" }]),
),
),
);
const provider = await registerSingleProviderPlugin(plugin);
await expect(
provider.prepareDynamicModel?.({
config: { models: { providers: { opencode: { baseUrl: "https://opencode.ai/zen/v1" } } } },
provider: "opencode",
modelId: "configured-selection-fixture",
modelRegistry: {},
} as never),
).resolves.toMatchObject({
id: "configured-selection-fixture",
api: "anthropic-messages",
baseUrl: "https://opencode.ai/zen",
});
expect(fetchMock).toHaveBeenCalledOnce();
const request = fetchMock.mock.calls[0];
expect(request?.[0]).toBe(METADATA_URL);
expect(new Headers(request?.[1]?.headers).has("authorization")).toBe(false);
});
it("does not mix provider-specific runtime auth with shared discovery auth", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new Error("unexpected fetch"));
const provider = await registerSingleProviderPlugin(plugin);
const result = await provider.catalog?.run({
config: {},
env: {},
resolveProviderApiKey: (providerId: string) =>
providerId === "opencode"
? { apiKey: NON_ENV_SECRETREF_MARKER, discoveryApiKey: undefined }
: { apiKey: "shared-opencode-key", discoveryApiKey: "shared-opencode-key" },
resolveProviderAuth: () => ({ apiKey: undefined, mode: "none", source: "none" }),
} as never);
if (!result || !("provider" in result)) {
throw new Error("expected OpenCode Zen provider result");
}
expect(result.provider.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
expectSeedModels(result.provider.models);
expect(fetchMock).not.toHaveBeenCalled();
});
it("discovers previously unknown trusted models and their upstream-owned transport", async () => {
const fetchGuard = createCatalogFetchGuard({
metadata: [
{ id: "x-preview-f-free", name: "Ox Alpha Free", reasoningEfforts: ["low", "high", "max"] },
{ id: "fresh-claude-fixture", npm: "@ai-sdk/anthropic" },
{ id: "fresh-gemini-fixture", npm: "@ai-sdk/google" },
{ id: "fresh-gpt-fixture", npm: "@ai-sdk/openai" },
{ id: "retired-zen-fixture", status: "deprecated" },
],
advertised: [
"x-preview-f-free",
"fresh-claude-fixture",
"fresh-gemini-fixture",
"fresh-gpt-fixture",
"retired-zen-fixture",
"untrusted-live-only-fixture",
],
});
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.resolveDynamicModel?.({ modelId: "x-preview-f-free" } as never),
).toBeUndefined();
const first = await buildOpencodeZenLiveProviderConfig({
apiKey: "runtime-key",
discoveryApiKey: "discovery-key",
fetchGuard,
});
const second = await buildOpencodeZenLiveProviderConfig({
apiKey: "other-runtime-key",
discoveryApiKey: "discovery-key",
fetchGuard,
});
expect(fetchGuard.mock.calls.map(([request]) => request.url)).toEqual([
METADATA_URL,
MODELS_URL,
]);
expect(first.models.map((model) => model.id)).toEqual([
"x-preview-f-free",
"fresh-claude-fixture",
"fresh-gemini-fixture",
"fresh-gpt-fixture",
]);
expect(second.apiKey).toBe("other-runtime-key");
expect(second.models.map((model) => model.id)).toEqual(first.models.map((model) => model.id));
expect(first.models.find((model) => model.id === "x-preview-f-free")).toMatchObject({
name: "Ox Alpha Free",
api: "openai-completions",
input: ["text", "image"],
contextWindow: 1_000_000,
contextTokens: 900_000,
maxTokens: 131_072,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
compat: { supportedReasoningEfforts: ["low", "high", "max"] },
});
expect(first.models.find((model) => model.id === "fresh-claude-fixture")).toMatchObject({
api: "anthropic-messages",
baseUrl: "https://opencode.ai/zen",
});
expect(first.models.find((model) => model.id === "fresh-gemini-fixture")).toMatchObject({
api: "google-generative-ai",
baseUrl: "https://opencode.ai/zen/v1",
});
expect(first.models.find((model) => model.id === "fresh-gpt-fixture")).toMatchObject({
api: "openai-responses",
baseUrl: "https://opencode.ai/zen/v1",
});
expect(
provider.resolveDynamicModel?.({ modelId: "retired-zen-fixture" } as never),
).toMatchObject({
id: "retired-zen-fixture",
});
expect(
provider.resolveDynamicModel?.({ modelId: "untrusted-live-only-fixture" } as never),
).toBeUndefined();
});
it("prepares explicitly selected upstream models without guessing names or protocol", async () => {
const fetchGuard = createCatalogFetchGuard({
metadata: [{ id: "selected-provider-fixture", npm: "@ai-sdk/anthropic" }],
advertised: [],
});
await expect(
prepareOpencodeZenModel({ modelId: "selected-provider-fixture", fetchGuard }),
).resolves.toMatchObject({
id: "selected-provider-fixture",
api: "anthropic-messages",
baseUrl: "https://opencode.ai/zen",
});
expect(fetchGuard.mock.calls.map(([request]) => request.url)).toEqual([METADATA_URL]);
});
it("evicts dynamic models removed or rejected by a refreshed authoritative catalog", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const fetchGuard = createCatalogFetchGuard({
metadata: [{ id: "refreshed-model-fixture" }],
advertised: [],
});
await prepareOpencodeZenModel({ modelId: "refreshed-model-fixture", fetchGuard });
expect(
provider.resolveDynamicModel?.({ modelId: "refreshed-model-fixture" } as never),
).toMatchObject({
id: "refreshed-model-fixture",
});
clearLiveCatalogCacheForTests();
const removedFetchGuard = createCatalogFetchGuard({ metadata: [], advertised: [] });
await prepareOpencodeZenModel({
modelId: "refreshed-model-fixture",
fetchGuard: removedFetchGuard,
});
expect(
provider.resolveDynamicModel?.({ modelId: "refreshed-model-fixture" } as never),
).toBeUndefined();
clearLiveCatalogCacheForTests();
await prepareOpencodeZenModel({ modelId: "refreshed-model-fixture", fetchGuard });
clearLiveCatalogCacheForTests();
const unsupportedFetchGuard = createCatalogFetchGuard({
metadata: [{ id: "refreshed-model-fixture", npm: "@untrusted/unknown" }],
advertised: [],
});
await prepareOpencodeZenModel({
modelId: "refreshed-model-fixture",
fetchGuard: unsupportedFetchGuard,
});
expect(
provider.resolveDynamicModel?.({ modelId: "refreshed-model-fixture" } as never),
).toBeUndefined();
expect(provider.resolveDynamicModel?.({ modelId: "gpt-5-nano" } as never)).toMatchObject({
id: "gpt-5-nano",
});
});
it("shares upstream metadata while keeping advertised models scoped to discovery keys", async () => {
const fetchGuard = createCatalogFetchGuard({
metadata: [{ id: "account-a-fixture" }, { id: "account-b-fixture" }],
advertised: (authorization) =>
authorization === "Bearer discovery-a" ? ["account-a-fixture"] : ["account-b-fixture"],
});
const first = await buildOpencodeZenLiveProviderConfig({
apiKey: "runtime-a",
discoveryApiKey: "discovery-a",
fetchGuard,
});
const second = await buildOpencodeZenLiveProviderConfig({
apiKey: "runtime-b",
discoveryApiKey: "discovery-b",
fetchGuard,
});
expect(first.models.map((model) => model.id)).toEqual(["account-a-fixture"]);
expect(second.models.map((model) => model.id)).toEqual(["account-b-fixture"]);
expect(fetchGuard.mock.calls.map(([request]) => request.url)).toEqual([
METADATA_URL,
MODELS_URL,
MODELS_URL,
]);
});
it("retains the compact offline seed when discovery fails", async () => {
const fetchGuard = vi.fn(async () => {
throw new Error("network unavailable");
});
const fallback = await buildOpencodeZenLiveProviderConfig({
apiKey: "runtime-key",
discoveryApiKey: "discovery-key",
fetchGuard,
});
expect(fallback.apiKey).toBe("runtime-key");
expectSeedModels(fallback.models);
});
it.each([
[["claude-opus-5"], "opencode/claude-opus-5"],
[["gpt-5.6-sol"], undefined],
])("selects only the advertised preferred onboarding model %#", async (modelIds, expected) => {
const fetchGuard = vi.fn(async () =>
guardedResponse(MODELS_URL, {
data: modelIds.map((id) => ({ id, object: "model" })),
}),
);
await expect(
resolveOpencodeZenStarterModel({
apiKey: "resolved-opencode-key",
preferredModelRef: "opencode/claude-opus-5",
fetchGuard,
}),
).resolves.toBe(expected);
});
it.each([
["off", undefined],
["max", "max"],
] as const)("keeps Kimi K3 reasoning %s exact", async (thinkingLevel, expectedEffort) => {
const provider = await registerSingleProviderPlugin(plugin);
const capturedPayloads: Record<string, unknown>[] = [];
const baseStreamFn = (_model: unknown, _context: unknown, options: unknown) => {
const payload: Record<string, unknown> = { model: "kimi-k3", reasoning_effort: "max" };
(options as { onPayload?: (payload: Record<string, unknown>) => void })?.onPayload?.(payload);
capturedPayloads.push(payload);
return {} as never;
};
const streamFn = provider.wrapStreamFn?.({
streamFn: baseStreamFn as never,
providerId: "opencode",
modelId: "kimi-k3",
thinkingLevel,
} as never);
await streamFn?.(
{ provider: "opencode", id: "kimi-k3", api: "openai-completions" } as never,
{} as never,
{},
);
expect(capturedPayloads).toEqual([
expectedEffort === undefined
? { model: "kimi-k3" }
: { model: "kimi-k3", reasoning_effort: expectedEffort },
]);
});
it("canonicalizes stale OpenCode Zen base URLs", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const normalizedConfig = requireRecord(
provider.normalizeConfig?.({
provider: "opencode",
providerConfig: {
api: "openai-completions",
baseUrl: "https://opencode.ai/zen/",
models: [],
},
} as never),
"normalized config",
);
expect(normalizedConfig.baseUrl).toBe("https://opencode.ai/zen/v1");
const normalizedModel = requireRecord(
provider.normalizeResolvedModel?.({
provider: "opencode",
model: {
provider: "opencode",
id: "claude-opus-5",
api: "anthropic-messages",
baseUrl: MODELS_URL.replace("/models", ""),
},
} as never),
"normalized model",
);
expect(normalizedModel.baseUrl).toBe("https://opencode.ai/zen");
expect(
provider.normalizeTransport?.({
provider: "opencode",
api: "openai-completions",
baseUrl: "https://opencode.ai/zen",
} as never),
).toEqual({ api: "openai-completions", baseUrl: "https://opencode.ai/zen/v1" });
});
it("exposes provider-owned thinking levels for proxied models", async () => {
const { providers } = await registerProviderPlugin({
plugin,
id: "opencode",
name: "OpenCode Zen Provider",
});
const provider = requireRegisteredProvider(providers, "opencode");
const resolveThinkingProfile = provider.resolveThinkingProfile;
if (!resolveThinkingProfile) {
throw new Error("expected OpenCode provider thinking profile");
}
const opusProfile = resolveThinkingProfile({
provider: "opencode",
modelId: "claude-opus-4-7",
});
expect(opusProfile?.levels.map((level) => level.id)).toContain("adaptive");
expect(
resolveThinkingProfile({
provider: "opencode",
modelId: "kimi-k3",
api: "openai-completions",
reasoning: true,
compat: { supportedReasoningEfforts: ["max"] },
}),
).toEqual({ levels: [{ id: "off" }, { id: "max" }], defaultLevel: "off" });
expect(
resolveThinkingProfile({
provider: "opencode",
modelId: "big-pickle",
api: "openai-completions",
reasoning: true,
}),
).toEqual({ levels: [{ id: "off", label: "always on" }], defaultLevel: "off" });
});
});