mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
63d9c69c11
* 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
647 lines
23 KiB
TypeScript
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" });
|
|
});
|
|
});
|