openclaw/src/plugins/provider-runtime.test.ts

431 lines
12 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderPlugin, ProviderRuntimeModel } from "./types.js";
const resolvePluginProvidersMock = vi.fn((_: unknown) => [] as ProviderPlugin[]);
const resolveOwningPluginIdsForProviderMock = vi.fn(
(_: unknown) => undefined as string[] | undefined,
);
vi.mock("./providers.js", () => ({
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
resolveOwningPluginIdsForProvider: (params: unknown) =>
resolveOwningPluginIdsForProviderMock(params as never),
}));
import {
augmentModelCatalogWithProviderPlugins,
buildProviderAuthDoctorHintWithPlugin,
buildProviderMissingAuthMessageWithPlugin,
formatProviderAuthProfileApiKeyWithPlugin,
prepareProviderExtraParams,
resolveProviderCacheTtlEligibility,
resolveProviderBinaryThinking,
resolveProviderBuiltInModelSuppression,
resolveProviderDefaultThinkingLevel,
resolveProviderModernModelRef,
resolveProviderUsageSnapshotWithPlugin,
resolveProviderCapabilitiesWithPlugin,
resolveProviderUsageAuthWithPlugin,
resolveProviderXHighThinking,
normalizeProviderResolvedModelWithPlugin,
prepareProviderDynamicModel,
prepareProviderRuntimeAuth,
refreshProviderOAuthCredentialWithPlugin,
resolveProviderRuntimePlugin,
runProviderDynamicModel,
wrapProviderStreamFn,
} from "./provider-runtime.js";
const MODEL: ProviderRuntimeModel = {
id: "demo-model",
name: "Demo Model",
api: "openai-responses",
provider: "demo",
baseUrl: "https://api.example.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 8_192,
};
describe("provider-runtime", () => {
beforeEach(() => {
resolvePluginProvidersMock.mockReset();
resolvePluginProvidersMock.mockReturnValue([]);
resolveOwningPluginIdsForProviderMock.mockReset();
resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined);
});
it("matches providers by alias for runtime hook lookup", () => {
resolvePluginProvidersMock.mockReturnValue([
{
id: "openrouter",
label: "OpenRouter",
aliases: ["Open Router"],
auth: [],
},
]);
const plugin = resolveProviderRuntimePlugin({ provider: "Open Router" });
expect(plugin?.id).toBe("openrouter");
expect(resolveOwningPluginIdsForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "Open Router",
}),
);
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
expect.objectContaining({
bundledProviderAllowlistCompat: true,
bundledProviderVitestCompat: true,
}),
);
});
it("dispatches runtime hooks for the matched provider", async () => {
const prepareDynamicModel = vi.fn(async () => undefined);
const prepareRuntimeAuth = vi.fn(async () => ({
apiKey: "runtime-token",
baseUrl: "https://runtime.example.com/v1",
expiresAt: 123,
}));
const refreshOAuth = vi.fn(async (cred) => ({
...cred,
access: "refreshed-access-token",
}));
const resolveUsageAuth = vi.fn(async () => ({
token: "usage-token",
accountId: "usage-account",
}));
const fetchUsageSnapshot = vi.fn(async () => ({
provider: "zai" as const,
displayName: "Demo",
windows: [{ label: "Day", usedPercent: 25 }],
}));
resolvePluginProvidersMock.mockImplementation((_params: unknown) => {
return [
{
id: "demo",
label: "Demo",
auth: [],
resolveDynamicModel: () => MODEL,
prepareDynamicModel,
capabilities: {
providerFamily: "openai",
},
prepareExtraParams: ({ extraParams }) => ({
...extraParams,
transport: "auto",
}),
wrapStreamFn: ({ streamFn }) => streamFn,
normalizeResolvedModel: ({ model }) => ({
...model,
api: "openai-codex-responses",
}),
formatApiKey: (cred) =>
cred.type === "oauth" ? JSON.stringify({ token: cred.access }) : "",
refreshOAuth,
buildAuthDoctorHint: ({ provider, profileId }) =>
provider === "demo" ? `Repair ${profileId}` : undefined,
prepareRuntimeAuth,
resolveUsageAuth,
fetchUsageSnapshot,
isCacheTtlEligible: ({ modelId }) => modelId.startsWith("anthropic/"),
isBinaryThinking: () => true,
supportsXHighThinking: ({ modelId }) => modelId === "gpt-5.4",
resolveDefaultThinkingLevel: ({ reasoning }) => (reasoning ? "low" : "off"),
isModernModelRef: ({ modelId }) => modelId.startsWith("gpt-5"),
},
{
id: "openai",
label: "OpenAI",
auth: [],
buildMissingAuthMessage: () =>
'No API key found for provider "openai". Use openai-codex/gpt-5.4.',
suppressBuiltInModel: ({ provider, modelId }) =>
provider === "azure-openai-responses" && modelId === "gpt-5.3-codex-spark"
? { suppress: true, errorMessage: "openai-codex/gpt-5.3-codex-spark" }
: undefined,
augmentModelCatalog: () => [
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
{
provider: "openai-codex",
id: "gpt-5.3-codex-spark",
name: "gpt-5.3-codex-spark",
},
],
},
];
});
expect(
runProviderDynamicModel({
provider: "demo",
context: {
provider: "demo",
modelId: MODEL.id,
modelRegistry: { find: () => null } as never,
},
}),
).toMatchObject(MODEL);
await prepareProviderDynamicModel({
provider: "demo",
context: {
provider: "demo",
modelId: MODEL.id,
modelRegistry: { find: () => null } as never,
},
});
expect(
resolveProviderCapabilitiesWithPlugin({
provider: "demo",
}),
).toMatchObject({
providerFamily: "openai",
});
expect(
prepareProviderExtraParams({
provider: "demo",
context: {
provider: "demo",
modelId: MODEL.id,
extraParams: { temperature: 0.3 },
},
}),
).toMatchObject({
temperature: 0.3,
transport: "auto",
});
expect(
wrapProviderStreamFn({
provider: "demo",
context: {
provider: "demo",
modelId: MODEL.id,
streamFn: vi.fn(),
},
}),
).toBeTypeOf("function");
expect(
normalizeProviderResolvedModelWithPlugin({
provider: "demo",
context: {
provider: "demo",
modelId: MODEL.id,
model: MODEL,
},
}),
).toMatchObject({
...MODEL,
api: "openai-codex-responses",
});
await expect(
prepareProviderRuntimeAuth({
provider: "demo",
env: process.env,
context: {
env: process.env,
provider: "demo",
modelId: MODEL.id,
model: MODEL,
apiKey: "source-token",
authMode: "api-key",
},
}),
).resolves.toMatchObject({
apiKey: "runtime-token",
baseUrl: "https://runtime.example.com/v1",
expiresAt: 123,
});
expect(
formatProviderAuthProfileApiKeyWithPlugin({
provider: "demo",
context: {
type: "oauth",
provider: "demo",
access: "oauth-access",
refresh: "oauth-refresh",
expires: Date.now() + 60_000,
},
}),
).toBe('{"token":"oauth-access"}');
await expect(
refreshProviderOAuthCredentialWithPlugin({
provider: "demo",
context: {
type: "oauth",
provider: "demo",
access: "oauth-access",
refresh: "oauth-refresh",
expires: Date.now() + 60_000,
},
}),
).resolves.toMatchObject({
access: "refreshed-access-token",
});
await expect(
buildProviderAuthDoctorHintWithPlugin({
provider: "demo",
context: {
provider: "demo",
profileId: "demo:default",
store: { version: 1, profiles: {} },
},
}),
).resolves.toBe("Repair demo:default");
await expect(
resolveProviderUsageAuthWithPlugin({
provider: "demo",
env: process.env,
context: {
config: {} as never,
env: process.env,
provider: "demo",
resolveApiKeyFromConfigAndStore: () => "source-token",
resolveOAuthToken: async () => null,
},
}),
).resolves.toMatchObject({
token: "usage-token",
accountId: "usage-account",
});
await expect(
resolveProviderUsageSnapshotWithPlugin({
provider: "demo",
env: process.env,
context: {
config: {} as never,
env: process.env,
provider: "demo",
token: "usage-token",
timeoutMs: 5_000,
fetchFn: vi.fn() as never,
},
}),
).resolves.toMatchObject({
provider: "zai",
windows: [{ label: "Day", usedPercent: 25 }],
});
expect(
resolveProviderCacheTtlEligibility({
provider: "demo",
context: {
provider: "demo",
modelId: "anthropic/claude-sonnet-4-5",
},
}),
).toBe(true);
expect(
resolveProviderBinaryThinking({
provider: "demo",
context: {
provider: "demo",
modelId: "glm-5",
},
}),
).toBe(true);
expect(
resolveProviderXHighThinking({
provider: "demo",
context: {
provider: "demo",
modelId: "gpt-5.4",
},
}),
).toBe(true);
expect(
resolveProviderDefaultThinkingLevel({
provider: "demo",
context: {
provider: "demo",
modelId: "gpt-5.4",
reasoning: true,
},
}),
).toBe("low");
expect(
resolveProviderModernModelRef({
provider: "demo",
context: {
provider: "demo",
modelId: "gpt-5.4",
},
}),
).toBe(true);
expect(
buildProviderMissingAuthMessageWithPlugin({
provider: "openai",
env: process.env,
context: {
env: process.env,
provider: "openai",
listProfileIds: (providerId) => (providerId === "openai-codex" ? ["p1"] : []),
},
}),
).toContain("openai-codex/gpt-5.4");
expect(
resolveProviderBuiltInModelSuppression({
env: process.env,
context: {
env: process.env,
provider: "azure-openai-responses",
modelId: "gpt-5.3-codex-spark",
},
}),
).toMatchObject({
suppress: true,
errorMessage: expect.stringContaining("openai-codex/gpt-5.3-codex-spark"),
});
await expect(
augmentModelCatalogWithProviderPlugins({
env: process.env,
context: {
env: process.env,
entries: [
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
{ provider: "openai", id: "gpt-5.2-pro", name: "GPT-5.2 Pro" },
{ provider: "openai-codex", id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
],
},
}),
).resolves.toEqual([
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
{
provider: "openai-codex",
id: "gpt-5.3-codex-spark",
name: "gpt-5.3-codex-spark",
},
]);
expect(prepareDynamicModel).toHaveBeenCalledTimes(1);
expect(refreshOAuth).toHaveBeenCalledTimes(1);
expect(prepareRuntimeAuth).toHaveBeenCalledTimes(1);
expect(resolveUsageAuth).toHaveBeenCalledTimes(1);
expect(fetchUsageSnapshot).toHaveBeenCalledTimes(1);
});
});