mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 00:00:44 +00:00
2195 lines
70 KiB
TypeScript
2195 lines
70 KiB
TypeScript
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { ModelProviderConfig, OpenClawConfig } from "../config/types.js";
|
|
import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js";
|
|
import {
|
|
expectAugmentedCodexCatalog,
|
|
expectCodexMissingAuthHint,
|
|
expectedAugmentedOpenaiCodexCatalogEntries,
|
|
} from "./provider-runtime.test-support.js";
|
|
import type {
|
|
AnyAgentTool,
|
|
ProviderExternalAuthProfile,
|
|
ProviderNormalizeToolSchemasContext,
|
|
ProviderPlugin,
|
|
ProviderSanitizeReplayHistoryContext,
|
|
ProviderValidateReplayTurnsContext,
|
|
} from "./types.js";
|
|
|
|
type ResolvePluginProviders = typeof import("./providers.runtime.js").resolvePluginProviders;
|
|
type IsPluginProvidersLoadInFlight =
|
|
typeof import("./providers.runtime.js").isPluginProvidersLoadInFlight;
|
|
type ResolveCatalogHookProviderPluginIds =
|
|
typeof import("./providers.js").resolveCatalogHookProviderPluginIds;
|
|
type ResolveExternalAuthProfileCompatFallbackPluginIds =
|
|
typeof import("./providers.js").resolveExternalAuthProfileCompatFallbackPluginIds;
|
|
type ResolveExternalAuthProfileProviderPluginIds =
|
|
typeof import("./providers.js").resolveExternalAuthProfileProviderPluginIds;
|
|
type ResolveOwningPluginIdsForProvider =
|
|
typeof import("./providers.js").resolveOwningPluginIdsForProvider;
|
|
type ResolveBundledProviderPolicySurface =
|
|
typeof import("./provider-public-artifacts.js").resolveBundledProviderPolicySurface;
|
|
|
|
const resolvePluginProvidersMock = vi.fn<ResolvePluginProviders>((_) => [] as ProviderPlugin[]);
|
|
const isPluginProvidersLoadInFlightMock = vi.fn<IsPluginProvidersLoadInFlight>((_) => false);
|
|
const resolveCatalogHookProviderPluginIdsMock = vi.fn<ResolveCatalogHookProviderPluginIds>(
|
|
(_) => [] as string[],
|
|
);
|
|
const resolveExternalAuthProfileCompatFallbackPluginIdsMock =
|
|
vi.fn<ResolveExternalAuthProfileCompatFallbackPluginIds>((_) => [] as string[]);
|
|
const resolveExternalAuthProfileProviderPluginIdsMock =
|
|
vi.fn<ResolveExternalAuthProfileProviderPluginIds>((_) => [] as string[]);
|
|
const resolveOwningPluginIdsForProviderMock = vi.fn<ResolveOwningPluginIdsForProvider>(
|
|
(_) => undefined,
|
|
);
|
|
const resolveBundledProviderPolicySurfaceMock = vi.fn<ResolveBundledProviderPolicySurface>(
|
|
(_) => null,
|
|
);
|
|
const providerRuntimeWarnMock = vi.fn();
|
|
|
|
let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins;
|
|
let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js").buildProviderAuthDoctorHintWithPlugin;
|
|
let buildProviderMissingAuthMessageWithPlugin: typeof import("./provider-runtime.js").buildProviderMissingAuthMessageWithPlugin;
|
|
let buildProviderUnknownModelHintWithPlugin: typeof import("./provider-runtime.js").buildProviderUnknownModelHintWithPlugin;
|
|
let applyProviderNativeStreamingUsageCompatWithPlugin: typeof import("./provider-runtime.js").applyProviderNativeStreamingUsageCompatWithPlugin;
|
|
let applyProviderConfigDefaultsWithPlugin: typeof import("./provider-runtime.js").applyProviderConfigDefaultsWithPlugin;
|
|
let formatProviderAuthProfileApiKeyWithPlugin: typeof import("./provider-runtime.js").formatProviderAuthProfileApiKeyWithPlugin;
|
|
let classifyProviderFailoverReasonWithPlugin: typeof import("./provider-runtime.js").classifyProviderFailoverReasonWithPlugin;
|
|
let matchesProviderContextOverflowWithPlugin: typeof import("./provider-runtime.js").matchesProviderContextOverflowWithPlugin;
|
|
let normalizeProviderConfigWithPlugin: typeof import("./provider-runtime.js").normalizeProviderConfigWithPlugin;
|
|
let normalizeProviderModelIdWithPlugin: typeof import("./provider-runtime.js").normalizeProviderModelIdWithPlugin;
|
|
let applyProviderResolvedModelCompatWithPlugins: typeof import("./provider-runtime.js").applyProviderResolvedModelCompatWithPlugins;
|
|
let applyProviderResolvedTransportWithPlugin: typeof import("./provider-runtime.js").applyProviderResolvedTransportWithPlugin;
|
|
let normalizeProviderTransportWithPlugin: typeof import("./provider-runtime.js").normalizeProviderTransportWithPlugin;
|
|
let prepareProviderExtraParams: typeof import("./provider-runtime.js").prepareProviderExtraParams;
|
|
let resolveProviderAuthProfileId: typeof import("./provider-runtime.js").resolveProviderAuthProfileId;
|
|
let resolveProviderConfigApiKeyWithPlugin: typeof import("./provider-runtime.js").resolveProviderConfigApiKeyWithPlugin;
|
|
let resolveProviderExtraParamsForTransport: typeof import("./provider-runtime.js").resolveProviderExtraParamsForTransport;
|
|
let resolveProviderFollowupFallbackRoute: typeof import("./provider-runtime.js").resolveProviderFollowupFallbackRoute;
|
|
let resolveProviderStreamFn: typeof import("./provider-runtime.js").resolveProviderStreamFn;
|
|
let resolveProviderCacheTtlEligibility: typeof import("./provider-runtime.js").resolveProviderCacheTtlEligibility;
|
|
let resolveProviderBinaryThinking: typeof import("./provider-runtime.js").resolveProviderBinaryThinking;
|
|
let createProviderEmbeddingProvider: typeof import("./provider-runtime.js").createProviderEmbeddingProvider;
|
|
let resolveProviderDefaultThinkingLevel: typeof import("./provider-runtime.js").resolveProviderDefaultThinkingLevel;
|
|
let resolveProviderModernModelRef: typeof import("./provider-runtime.js").resolveProviderModernModelRef;
|
|
let resolveProviderReasoningOutputModeWithPlugin: typeof import("./provider-runtime.js").resolveProviderReasoningOutputModeWithPlugin;
|
|
let resolveProviderReplayPolicyWithPlugin: typeof import("./provider-runtime.js").resolveProviderReplayPolicyWithPlugin;
|
|
let resolveProviderSystemPromptContribution: typeof import("./provider-runtime.js").resolveProviderSystemPromptContribution;
|
|
let resolveExternalAuthProfilesWithPlugins: typeof import("./provider-runtime.js").resolveExternalAuthProfilesWithPlugins;
|
|
let resolveProviderSyntheticAuthWithPlugin: typeof import("./provider-runtime.js").resolveProviderSyntheticAuthWithPlugin;
|
|
let shouldDeferProviderSyntheticProfileAuthWithPlugin: typeof import("./provider-runtime.js").shouldDeferProviderSyntheticProfileAuthWithPlugin;
|
|
let sanitizeProviderReplayHistoryWithPlugin: typeof import("./provider-runtime.js").sanitizeProviderReplayHistoryWithPlugin;
|
|
let resolveProviderUsageSnapshotWithPlugin: typeof import("./provider-runtime.js").resolveProviderUsageSnapshotWithPlugin;
|
|
let resolveProviderUsageAuthWithPlugin: typeof import("./provider-runtime.js").resolveProviderUsageAuthWithPlugin;
|
|
let resolveProviderXHighThinking: typeof import("./provider-runtime.js").resolveProviderXHighThinking;
|
|
let normalizeProviderToolSchemasWithPlugin: typeof import("./provider-runtime.js").normalizeProviderToolSchemasWithPlugin;
|
|
let inspectProviderToolSchemasWithPlugin: typeof import("./provider-runtime.js").inspectProviderToolSchemasWithPlugin;
|
|
let normalizeProviderResolvedModelWithPlugin: typeof import("./provider-runtime.js").normalizeProviderResolvedModelWithPlugin;
|
|
let prepareProviderDynamicModel: typeof import("./provider-runtime.js").prepareProviderDynamicModel;
|
|
let prepareProviderRuntimeAuth: typeof import("./provider-runtime.js").prepareProviderRuntimeAuth;
|
|
let resetProviderRuntimeHookCacheForTest: typeof import("./provider-runtime.js").resetProviderRuntimeHookCacheForTest;
|
|
let refreshProviderOAuthCredentialWithPlugin: typeof import("./provider-runtime.js").refreshProviderOAuthCredentialWithPlugin;
|
|
let resolveProviderRuntimePlugin: typeof import("./provider-runtime.js").resolveProviderRuntimePlugin;
|
|
let providerRuntimeTesting: typeof import("./provider-runtime.js").__testing;
|
|
let runProviderDynamicModel: typeof import("./provider-runtime.js").runProviderDynamicModel;
|
|
let validateProviderReplayTurnsWithPlugin: typeof import("./provider-runtime.js").validateProviderReplayTurnsWithPlugin;
|
|
let wrapProviderStreamFn: typeof import("./provider-runtime.js").wrapProviderStreamFn;
|
|
|
|
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,
|
|
};
|
|
const DEMO_PROVIDER_ID = "demo";
|
|
const EMPTY_MODEL_REGISTRY = { find: () => null } as never;
|
|
const DEMO_REPLAY_MESSAGES: AgentMessage[] = [{ role: "user", content: "hello", timestamp: 1 }];
|
|
const DEMO_SANITIZED_MESSAGE: AgentMessage = {
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "sanitized" }],
|
|
api: MODEL.api,
|
|
provider: MODEL.provider,
|
|
model: MODEL.id,
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: 2,
|
|
};
|
|
const DEMO_TOOL = {
|
|
name: "demo-tool",
|
|
label: "Demo tool",
|
|
description: "Demo tool",
|
|
parameters: { type: "object", properties: {} },
|
|
execute: vi.fn(async () => ({ content: [], details: undefined })),
|
|
} as unknown as AnyAgentTool;
|
|
|
|
function createOpenAiCatalogProviderPlugin(
|
|
overrides: Partial<ProviderPlugin> = {},
|
|
): ProviderPlugin {
|
|
return {
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
auth: [],
|
|
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", id: "gpt-5.4-mini", name: "gpt-5.4-mini" },
|
|
{ provider: "openai", id: "gpt-5.4-nano", name: "gpt-5.4-nano" },
|
|
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
|
|
{ provider: "openai-codex", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
|
|
{ provider: "openai-codex", id: "gpt-5.4-mini", name: "gpt-5.4-mini" },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function expectProviderRuntimePluginLoad(params: { provider: string; expectedPluginId?: string }) {
|
|
const plugin = resolveProviderRuntimePlugin({ provider: params.provider });
|
|
|
|
expect(plugin?.id).toBe(params.expectedPluginId);
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
providerRefs: [params.provider],
|
|
bundledProviderAllowlistCompat: true,
|
|
bundledProviderVitestCompat: true,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function createDemoRuntimeContext<TContext extends Record<string, unknown>>(
|
|
overrides: TContext,
|
|
): TContext & { provider: string; modelId: string } {
|
|
return {
|
|
provider: DEMO_PROVIDER_ID,
|
|
modelId: MODEL.id,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDemoProviderContext<TContext extends Record<string, unknown>>(
|
|
overrides: TContext,
|
|
): TContext & { provider: string } {
|
|
return {
|
|
provider: DEMO_PROVIDER_ID,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDemoResolvedModelContext<TContext extends Record<string, unknown>>(
|
|
overrides: TContext,
|
|
): TContext & { provider: string; modelId: string; model: ProviderRuntimeModel } {
|
|
return createDemoRuntimeContext({
|
|
model: MODEL,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function expectCalledOnce(...mocks: Array<{ mock: { calls: unknown[] } }>) {
|
|
for (const mockFn of mocks) {
|
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
}
|
|
}
|
|
|
|
function expectResolvedValues(
|
|
cases: ReadonlyArray<{
|
|
actual: () => unknown;
|
|
expected: unknown;
|
|
}>,
|
|
) {
|
|
cases.forEach(({ actual, expected }) => {
|
|
expect(actual()).toEqual(expected);
|
|
});
|
|
}
|
|
|
|
async function expectResolvedMatches(
|
|
cases: ReadonlyArray<{
|
|
actual: () => Promise<unknown>;
|
|
expected: Record<string, unknown>;
|
|
}>,
|
|
) {
|
|
await Promise.all(
|
|
cases.map(async ({ actual, expected }) => {
|
|
await expect(actual()).resolves.toMatchObject(expected);
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function expectResolvedAsyncValues(
|
|
cases: ReadonlyArray<{
|
|
actual: () => Promise<unknown>;
|
|
expected: unknown;
|
|
}>,
|
|
) {
|
|
await Promise.all(
|
|
cases.map(async ({ actual, expected }) => {
|
|
await expect(actual()).resolves.toEqual(expected);
|
|
}),
|
|
);
|
|
}
|
|
|
|
describe("provider-runtime", () => {
|
|
beforeAll(async () => {
|
|
vi.resetModules();
|
|
vi.doMock("./provider-public-artifacts.js", () => ({
|
|
resolveBundledProviderPolicySurface: (provider: string) =>
|
|
resolveBundledProviderPolicySurfaceMock(provider),
|
|
}));
|
|
vi.doMock("./providers.js", () => ({
|
|
resolveCatalogHookProviderPluginIds: (params: unknown) =>
|
|
resolveCatalogHookProviderPluginIdsMock(params as never),
|
|
resolveExternalAuthProfileCompatFallbackPluginIds: (params: unknown) =>
|
|
resolveExternalAuthProfileCompatFallbackPluginIdsMock(params as never),
|
|
resolveExternalAuthProfileProviderPluginIds: (params: unknown) =>
|
|
resolveExternalAuthProfileProviderPluginIdsMock(params as never),
|
|
resolveOwningPluginIdsForProvider: (params: unknown) =>
|
|
resolveOwningPluginIdsForProviderMock(params as never),
|
|
}));
|
|
vi.doMock("./providers.runtime.js", () => ({
|
|
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
|
|
isPluginProvidersLoadInFlight: (params: unknown) =>
|
|
isPluginProvidersLoadInFlightMock(params as never),
|
|
}));
|
|
vi.doMock("../logging/subsystem.js", () => ({
|
|
createSubsystemLogger: () => ({
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: providerRuntimeWarnMock,
|
|
error: vi.fn(),
|
|
}),
|
|
}));
|
|
({
|
|
augmentModelCatalogWithProviderPlugins,
|
|
buildProviderAuthDoctorHintWithPlugin,
|
|
buildProviderMissingAuthMessageWithPlugin,
|
|
buildProviderUnknownModelHintWithPlugin,
|
|
applyProviderNativeStreamingUsageCompatWithPlugin,
|
|
applyProviderConfigDefaultsWithPlugin,
|
|
applyProviderResolvedModelCompatWithPlugins,
|
|
applyProviderResolvedTransportWithPlugin,
|
|
classifyProviderFailoverReasonWithPlugin,
|
|
formatProviderAuthProfileApiKeyWithPlugin,
|
|
matchesProviderContextOverflowWithPlugin,
|
|
normalizeProviderConfigWithPlugin,
|
|
normalizeProviderModelIdWithPlugin,
|
|
normalizeProviderTransportWithPlugin,
|
|
prepareProviderExtraParams,
|
|
resolveProviderAuthProfileId,
|
|
resolveProviderConfigApiKeyWithPlugin,
|
|
resolveProviderExtraParamsForTransport,
|
|
resolveProviderFollowupFallbackRoute,
|
|
resolveProviderStreamFn,
|
|
resolveProviderCacheTtlEligibility,
|
|
resolveProviderBinaryThinking,
|
|
createProviderEmbeddingProvider,
|
|
resolveProviderDefaultThinkingLevel,
|
|
resolveProviderModernModelRef,
|
|
resolveProviderReasoningOutputModeWithPlugin,
|
|
resolveProviderReplayPolicyWithPlugin,
|
|
resolveProviderSystemPromptContribution,
|
|
resolveExternalAuthProfilesWithPlugins,
|
|
resolveProviderSyntheticAuthWithPlugin,
|
|
shouldDeferProviderSyntheticProfileAuthWithPlugin,
|
|
sanitizeProviderReplayHistoryWithPlugin,
|
|
resolveProviderUsageSnapshotWithPlugin,
|
|
resolveProviderUsageAuthWithPlugin,
|
|
resolveProviderXHighThinking,
|
|
normalizeProviderToolSchemasWithPlugin,
|
|
inspectProviderToolSchemasWithPlugin,
|
|
normalizeProviderResolvedModelWithPlugin,
|
|
prepareProviderDynamicModel,
|
|
prepareProviderRuntimeAuth,
|
|
resetProviderRuntimeHookCacheForTest,
|
|
refreshProviderOAuthCredentialWithPlugin,
|
|
resolveProviderRuntimePlugin,
|
|
__testing: providerRuntimeTesting,
|
|
runProviderDynamicModel,
|
|
validateProviderReplayTurnsWithPlugin,
|
|
wrapProviderStreamFn,
|
|
} = await import("./provider-runtime.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetProviderRuntimeHookCacheForTest();
|
|
providerRuntimeTesting.resetExternalAuthFallbackWarningCacheForTest();
|
|
providerRuntimeTesting.resetCatalogHookProvidersCacheForTest();
|
|
resolvePluginProvidersMock.mockReset();
|
|
resolvePluginProvidersMock.mockReturnValue([]);
|
|
isPluginProvidersLoadInFlightMock.mockReset();
|
|
isPluginProvidersLoadInFlightMock.mockReturnValue(false);
|
|
resolveCatalogHookProviderPluginIdsMock.mockReset();
|
|
resolveCatalogHookProviderPluginIdsMock.mockReturnValue([]);
|
|
resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReset();
|
|
resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReturnValue([]);
|
|
resolveExternalAuthProfileProviderPluginIdsMock.mockReset();
|
|
resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue([]);
|
|
resolveOwningPluginIdsForProviderMock.mockReset();
|
|
resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined);
|
|
resolveBundledProviderPolicySurfaceMock.mockReset();
|
|
resolveBundledProviderPolicySurfaceMock.mockReturnValue(null);
|
|
providerRuntimeWarnMock.mockReset();
|
|
});
|
|
|
|
it("matches providers by alias for runtime hook lookup", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "openrouter",
|
|
label: "OpenRouter",
|
|
aliases: ["Open Router"],
|
|
auth: [],
|
|
},
|
|
]);
|
|
|
|
expectProviderRuntimePluginLoad({
|
|
provider: "Open Router",
|
|
expectedPluginId: "openrouter",
|
|
});
|
|
});
|
|
|
|
it("matches providers by hook alias for runtime hook lookup", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "anthropic",
|
|
label: "Anthropic",
|
|
hookAliases: ["claude-cli"],
|
|
auth: [],
|
|
},
|
|
]);
|
|
|
|
expectProviderRuntimePluginLoad({
|
|
provider: "claude-cli",
|
|
expectedPluginId: "anthropic",
|
|
});
|
|
});
|
|
|
|
it("normalizes plugin scopes in provider hook cache keys", () => {
|
|
const base = {
|
|
workspaceDir: "/tmp/workspace",
|
|
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
|
providerRefs: ["demo"],
|
|
};
|
|
|
|
expect(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
onlyPluginIds: [" beta ", "alpha", "beta"],
|
|
}),
|
|
).toBe(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
onlyPluginIds: ["alpha", "beta"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("separates provider hook cache keys by load policy", () => {
|
|
const base = {
|
|
workspaceDir: "/tmp/workspace",
|
|
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
|
providerRefs: ["demo"],
|
|
};
|
|
|
|
expect(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
applyAutoEnable: false,
|
|
bundledProviderAllowlistCompat: false,
|
|
bundledProviderVitestCompat: false,
|
|
installBundledRuntimeDeps: false,
|
|
}),
|
|
).not.toBe(providerRuntimeTesting.buildHookProviderCacheKey(base));
|
|
});
|
|
|
|
it("ignores unrelated plugin config values in provider hook cache keys", () => {
|
|
const base = {
|
|
workspaceDir: "/tmp/workspace",
|
|
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
|
onlyPluginIds: ["demo"],
|
|
};
|
|
const firstConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
|
"active-memory": { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const secondConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
|
"active-memory": { enabled: true, config: { qmd: { searchMode: "fast" } } },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
config: firstConfig,
|
|
fullConfigPluginIds: ["demo"],
|
|
}),
|
|
).toBe(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
config: secondConfig,
|
|
fullConfigPluginIds: ["demo"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps scoped provider plugin config in provider hook cache keys", () => {
|
|
const base = {
|
|
workspaceDir: "/tmp/workspace",
|
|
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
|
onlyPluginIds: ["demo"],
|
|
fullConfigPluginIds: ["demo"],
|
|
};
|
|
|
|
expect(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://one.example" } },
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
}),
|
|
).not.toBe(
|
|
providerRuntimeTesting.buildHookProviderCacheKey({
|
|
...base,
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://two.example" } },
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps provider-ref owner plugin config in provider hook cache keys", () => {
|
|
const provider: ProviderPlugin = {
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
};
|
|
resolveOwningPluginIdsForProviderMock.mockReturnValue(["demo"]);
|
|
resolvePluginProvidersMock.mockReturnValue([provider]);
|
|
const firstConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://one.example" } },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const secondConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://two.example" } },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: firstConfig })).toBe(
|
|
provider,
|
|
);
|
|
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: secondConfig })).toBe(
|
|
provider,
|
|
);
|
|
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("reuses provider-ref hook loads when unrelated plugin config changes", () => {
|
|
const provider: ProviderPlugin = {
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
};
|
|
resolveOwningPluginIdsForProviderMock.mockReturnValue(["demo"]);
|
|
resolvePluginProvidersMock.mockReturnValue([provider]);
|
|
const firstConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
|
"active-memory": { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const secondConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
|
"active-memory": { enabled: true, config: { qmd: { searchMode: "fast" } } },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: firstConfig })).toBe(
|
|
provider,
|
|
);
|
|
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: secondConfig })).toBe(
|
|
provider,
|
|
);
|
|
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not reuse auto-enabled runtime providers for synthetic auth fallback", () => {
|
|
const runtimeProvider: ProviderPlugin = {
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
resolveSyntheticAuth: () => ({
|
|
apiKey: "default-runtime-token",
|
|
source: "default runtime",
|
|
mode: "api-key" as const,
|
|
}),
|
|
};
|
|
resolvePluginProvidersMock.mockImplementation((params) =>
|
|
params.applyAutoEnable === false &&
|
|
params.bundledProviderAllowlistCompat === false &&
|
|
params.bundledProviderVitestCompat === false &&
|
|
params.installBundledRuntimeDeps === false
|
|
? []
|
|
: [runtimeProvider],
|
|
);
|
|
|
|
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID })).toBe(runtimeProvider);
|
|
|
|
expect(
|
|
resolveProviderSyntheticAuthWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
providerConfig: {
|
|
api: "ollama",
|
|
baseUrl: "http://127.0.0.1:11434",
|
|
models: [],
|
|
},
|
|
},
|
|
}),
|
|
).toBeUndefined();
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("skips provider runtime loading when no plugin declares external auth hooks", () => {
|
|
expect(
|
|
resolveExternalAuthProfilesWithPlugins({
|
|
env: process.env,
|
|
context: {
|
|
env: process.env,
|
|
store: { version: 1, profiles: {} },
|
|
},
|
|
}),
|
|
).toEqual([]);
|
|
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("warns once with a log-safe plugin id for undeclared external auth fallback plugins", () => {
|
|
const unsafePluginId = "legacy-provider\nWARN forged";
|
|
resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReturnValue([unsafePluginId]);
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "legacy-provider",
|
|
pluginId: unsafePluginId,
|
|
label: "Legacy Provider",
|
|
auth: [],
|
|
resolveExternalOAuthProfiles: () => [
|
|
{
|
|
profileId: "legacy-provider:external",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "legacy-provider",
|
|
access: "access",
|
|
refresh: "refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
for (let i = 0; i < 2; i += 1) {
|
|
expect(
|
|
resolveExternalAuthProfilesWithPlugins({
|
|
env: process.env,
|
|
context: {
|
|
env: process.env,
|
|
store: { version: 1, profiles: {} },
|
|
},
|
|
}),
|
|
).toEqual([
|
|
expect.objectContaining({
|
|
profileId: "legacy-provider:external",
|
|
}),
|
|
]);
|
|
}
|
|
|
|
expect(providerRuntimeWarnMock).toHaveBeenCalledTimes(1);
|
|
const warning = String(providerRuntimeWarnMock.mock.calls[0]?.[0] ?? "");
|
|
expect(warning).toContain('Provider plugin "legacy-providerWARN forged"');
|
|
expect(warning).not.toContain("\n");
|
|
});
|
|
|
|
it("does not warn for declared external auth plugins with different provider ids", () => {
|
|
resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue(["demo-plugin"]);
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "demo-provider",
|
|
pluginId: "demo-plugin",
|
|
label: "Demo Provider",
|
|
auth: [],
|
|
resolveExternalAuthProfiles: () => [
|
|
{
|
|
profileId: "demo-provider:external",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "demo-provider",
|
|
access: "access",
|
|
refresh: "refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
resolveExternalAuthProfilesWithPlugins({
|
|
env: process.env,
|
|
context: {
|
|
env: process.env,
|
|
store: { version: 1, profiles: {} },
|
|
},
|
|
}),
|
|
).toEqual([
|
|
expect.objectContaining({
|
|
profileId: "demo-provider:external",
|
|
}),
|
|
]);
|
|
expect(providerRuntimeWarnMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reuses catalog hook provider loads when only non-plugin config changes", async () => {
|
|
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["demo"]);
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "demo",
|
|
label: "Demo",
|
|
auth: [],
|
|
augmentModelCatalog: () => [{ provider: "demo", id: "demo-model", name: "Demo Model" }],
|
|
},
|
|
]);
|
|
const baseConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const firstConfig = {
|
|
...baseConfig,
|
|
agents: { defaults: { model: "openai/gpt-5.4" } },
|
|
} as OpenClawConfig;
|
|
const secondConfig = {
|
|
...baseConfig,
|
|
agents: { defaults: { model: "anthropic/claude-sonnet-4-5" } },
|
|
} as OpenClawConfig;
|
|
|
|
expect(
|
|
await augmentModelCatalogWithProviderPlugins({
|
|
config: firstConfig,
|
|
env: process.env,
|
|
context: { config: firstConfig, env: process.env, entries: [] },
|
|
}),
|
|
).toEqual([{ provider: "demo", id: "demo-model", name: "Demo Model" }]);
|
|
expect(
|
|
await augmentModelCatalogWithProviderPlugins({
|
|
config: secondConfig,
|
|
env: process.env,
|
|
context: { config: secondConfig, env: process.env, entries: [] },
|
|
}),
|
|
).toEqual([{ provider: "demo", id: "demo-model", name: "Demo Model" }]);
|
|
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("reuses catalog hook provider loads when unrelated plugin config changes", async () => {
|
|
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["demo"]);
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "demo",
|
|
label: "Demo",
|
|
auth: [],
|
|
augmentModelCatalog: () => [{ provider: "demo", id: "demo-model", name: "Demo Model" }],
|
|
},
|
|
]);
|
|
const firstConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
|
"active-memory": { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const secondConfig = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
|
"active-memory": { enabled: true, config: { qmd: { searchMode: "fast" } } },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
for (const config of [firstConfig, secondConfig]) {
|
|
expect(
|
|
await augmentModelCatalogWithProviderPlugins({
|
|
config,
|
|
env: process.env,
|
|
context: { config, env: process.env, entries: [] },
|
|
}),
|
|
).toEqual([{ provider: "demo", id: "demo-model", name: "Demo Model" }]);
|
|
}
|
|
|
|
expect(resolveCatalogHookProviderPluginIdsMock).toHaveBeenCalledTimes(1);
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("returns provider-prepared runtime auth for the matched provider", async () => {
|
|
const prepareRuntimeAuth = vi.fn(async () => ({
|
|
apiKey: "runtime-token",
|
|
baseUrl: "https://runtime.example.com/v1",
|
|
expiresAt: 123,
|
|
}));
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
prepareRuntimeAuth,
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
prepareProviderRuntimeAuth({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
config: undefined,
|
|
workspaceDir: "/tmp/demo-workspace",
|
|
env: process.env,
|
|
provider: DEMO_PROVIDER_ID,
|
|
modelId: MODEL.id,
|
|
model: MODEL,
|
|
apiKey: "raw-token",
|
|
authMode: "token",
|
|
},
|
|
}),
|
|
).resolves.toEqual({
|
|
apiKey: "runtime-token",
|
|
baseUrl: "https://runtime.example.com/v1",
|
|
expiresAt: 123,
|
|
});
|
|
expect(prepareRuntimeAuth).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
apiKey: "raw-token",
|
|
modelId: MODEL.id,
|
|
provider: DEMO_PROVIDER_ID,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("returns no runtime plugin when the provider has no owning plugin", () => {
|
|
expectProviderRuntimePluginLoad({
|
|
provider: "anthropic",
|
|
});
|
|
});
|
|
|
|
it("exposes provider-owned transport extra params", () => {
|
|
const extraParamsForTransport = vi.fn((_ctx) => ({
|
|
patch: {
|
|
providerTransportPatch: true,
|
|
},
|
|
}));
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
extraParamsForTransport,
|
|
} satisfies ProviderPlugin,
|
|
]);
|
|
|
|
expect(
|
|
resolveProviderExtraParamsForTransport({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
extraParams: { transport: "websocket" },
|
|
transport: "websocket" as const,
|
|
}),
|
|
}),
|
|
).toEqual({
|
|
patch: {
|
|
providerTransportPatch: true,
|
|
},
|
|
});
|
|
expect(extraParamsForTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: DEMO_PROVIDER_ID,
|
|
modelId: MODEL.id,
|
|
model: MODEL,
|
|
transport: "websocket",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("exposes provider-owned auth profile and fallback route seams", () => {
|
|
const resolveAuthProfileId = vi.fn(() => "profile-b");
|
|
const followupFallbackRoute = vi.fn(() => ({
|
|
route: "dispatcher" as const,
|
|
reason: "origin unavailable",
|
|
}));
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
resolveAuthProfileId,
|
|
followupFallbackRoute,
|
|
} satisfies ProviderPlugin,
|
|
]);
|
|
|
|
expect(
|
|
resolveProviderAuthProfileId({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoRuntimeContext({
|
|
profileOrder: ["profile-a", "profile-b"],
|
|
authStore: { version: 1, profiles: {}, order: {} },
|
|
}),
|
|
}),
|
|
).toBe("profile-b");
|
|
expect(
|
|
resolveProviderFollowupFallbackRoute({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoRuntimeContext({
|
|
payload: { text: "hello" },
|
|
originRoutable: false,
|
|
dispatcherAvailable: true,
|
|
}),
|
|
}),
|
|
).toEqual({
|
|
route: "dispatcher",
|
|
reason: "origin unavailable",
|
|
});
|
|
});
|
|
|
|
it("applies the shared GPT-5 prompt overlay for any provider", () => {
|
|
const contribution = resolveProviderSystemPromptContribution({
|
|
provider: "openrouter",
|
|
context: {
|
|
provider: "openrouter",
|
|
modelId: "openai/gpt-5.4",
|
|
promptMode: "full",
|
|
} as never,
|
|
});
|
|
|
|
expect(contribution?.stablePrefix).toContain("<persona_latch>");
|
|
expect(contribution?.sectionOverrides?.interaction_style).toContain(
|
|
"This is a live chat, not a memo.",
|
|
);
|
|
});
|
|
|
|
it("respects the shared GPT-5 prompt overlay personality config", () => {
|
|
const contribution = resolveProviderSystemPromptContribution({
|
|
provider: "opencode",
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
promptOverlays: {
|
|
gpt5: { personality: "off" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
context: {
|
|
provider: "opencode",
|
|
modelId: "gpt-5.4",
|
|
promptMode: "full",
|
|
} as never,
|
|
});
|
|
|
|
expect(contribution?.stablePrefix).toContain("<persona_latch>");
|
|
expect(contribution?.sectionOverrides).toEqual({});
|
|
});
|
|
|
|
it("lets provider-owned prompt overlays compose after the built-in GPT-5 overlay", () => {
|
|
const resolvePromptOverlay = vi.fn((ctx) => ({
|
|
stablePrefix: "provider overlay",
|
|
sectionOverrides: {
|
|
execution_bias: ctx.baseOverlay?.stablePrefix ? "saw built-in overlay" : "missing",
|
|
},
|
|
}));
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "openrouter",
|
|
label: "OpenRouter",
|
|
auth: [],
|
|
resolvePromptOverlay,
|
|
} satisfies ProviderPlugin,
|
|
]);
|
|
|
|
const contribution = resolveProviderSystemPromptContribution({
|
|
provider: "openrouter",
|
|
context: {
|
|
provider: "openrouter",
|
|
modelId: "openai/gpt-5.4",
|
|
promptMode: "full",
|
|
} as never,
|
|
});
|
|
|
|
expect(contribution?.stablePrefix).toContain("<persona_latch>");
|
|
expect(contribution?.stablePrefix).toContain("provider overlay");
|
|
expect(contribution?.sectionOverrides?.execution_bias).toBe("saw built-in overlay");
|
|
expect(resolvePromptOverlay).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "openrouter",
|
|
modelId: "openai/gpt-5.4",
|
|
baseOverlay: expect.objectContaining({
|
|
stablePrefix: expect.stringContaining("<persona_latch>"),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("ignores OpenAI plugin personality fallback for non-OpenAI GPT-5 providers", () => {
|
|
const contribution = resolveProviderSystemPromptContribution({
|
|
provider: "openrouter",
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
openai: { config: { personality: "off" } },
|
|
},
|
|
},
|
|
},
|
|
context: {
|
|
provider: "openrouter",
|
|
modelId: "openai/gpt-5.4",
|
|
promptMode: "full",
|
|
} as never,
|
|
});
|
|
|
|
expect(contribution?.stablePrefix).toContain("<persona_latch>");
|
|
expect(contribution?.sectionOverrides?.interaction_style).toContain(
|
|
"This is a live chat, not a memo.",
|
|
);
|
|
});
|
|
|
|
it("keeps OpenAI plugin personality fallback for OpenAI-family GPT-5 providers", () => {
|
|
const contribution = resolveProviderSystemPromptContribution({
|
|
provider: "openai-codex",
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
openai: { config: { personality: "off" } },
|
|
},
|
|
},
|
|
},
|
|
context: {
|
|
provider: "openai-codex",
|
|
modelId: "gpt-5.4",
|
|
promptMode: "full",
|
|
} as never,
|
|
});
|
|
|
|
expect(contribution?.stablePrefix).toContain("<persona_latch>");
|
|
expect(contribution?.sectionOverrides).toEqual({});
|
|
});
|
|
|
|
it("keeps OpenAI plugin personality fallback for Azure OpenAI GPT-5 providers", () => {
|
|
const contribution = resolveProviderSystemPromptContribution({
|
|
provider: "azure-openai-responses",
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
openai: { config: { personality: "off" } },
|
|
},
|
|
},
|
|
},
|
|
context: {
|
|
provider: "azure-openai-responses",
|
|
modelId: "gpt-5.4",
|
|
promptMode: "full",
|
|
} as never,
|
|
});
|
|
|
|
expect(contribution?.stablePrefix).toContain("<persona_latch>");
|
|
expect(contribution?.sectionOverrides).toEqual({});
|
|
});
|
|
|
|
it("does not apply the shared GPT-5 prompt overlay to non-GPT-5 models", () => {
|
|
expect(
|
|
resolveProviderSystemPromptContribution({
|
|
provider: "openrouter",
|
|
context: {
|
|
provider: "openrouter",
|
|
modelId: "openai/gpt-4.1",
|
|
promptMode: "full",
|
|
} as never,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("can normalize model ids through provider aliases without changing ownership", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "google",
|
|
label: "Google",
|
|
hookAliases: ["google-vertex"],
|
|
auth: [],
|
|
normalizeModelId: ({ modelId }) => modelId.replace("flash-lite", "flash-lite-preview"),
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
normalizeProviderModelIdWithPlugin({
|
|
provider: "google-vertex",
|
|
context: {
|
|
provider: "google-vertex",
|
|
modelId: "gemini-3.1-flash-lite",
|
|
},
|
|
}),
|
|
).toBe("gemini-3.1-flash-lite-preview");
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("resolves config hooks through hook-only aliases without changing provider surfaces", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "google",
|
|
label: "Google",
|
|
hookAliases: ["google-antigravity"],
|
|
auth: [],
|
|
normalizeConfig: ({ providerConfig }) => ({
|
|
...providerConfig,
|
|
baseUrl: "https://normalized.example.com/v1",
|
|
}),
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
normalizeProviderConfigWithPlugin({
|
|
provider: "google-antigravity",
|
|
context: {
|
|
provider: "google-antigravity",
|
|
providerConfig: {
|
|
baseUrl: "https://example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
},
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
baseUrl: "https://normalized.example.com/v1",
|
|
});
|
|
});
|
|
|
|
it("does not scan provider plugins after bundled policy surface handles config", () => {
|
|
const providerConfig: ModelProviderConfig = {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
api: "openai-completions",
|
|
models: [],
|
|
};
|
|
const normalizeConfig = vi.fn(() => providerConfig);
|
|
resolveBundledProviderPolicySurfaceMock.mockReturnValue({
|
|
normalizeConfig,
|
|
});
|
|
|
|
expect(
|
|
normalizeProviderConfigWithPlugin({
|
|
provider: "openai",
|
|
context: {
|
|
provider: "openai",
|
|
providerConfig,
|
|
},
|
|
}),
|
|
).toBeUndefined();
|
|
|
|
expect(normalizeConfig).toHaveBeenCalledTimes(1);
|
|
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves provider config defaults through owner plugins", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "anthropic",
|
|
label: "Anthropic",
|
|
auth: [],
|
|
applyConfigDefaults: ({ config }) => ({
|
|
...config,
|
|
agents: {
|
|
defaults: {
|
|
heartbeat: { every: "1h" },
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
applyProviderConfigDefaultsWithPlugin({
|
|
provider: "anthropic",
|
|
context: {
|
|
provider: "anthropic",
|
|
env: {},
|
|
config: {},
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
agents: {
|
|
defaults: {
|
|
heartbeat: {
|
|
every: "1h",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("resolves failover classification through hook-only aliases", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
hookAliases: ["azure-openai-responses"],
|
|
auth: [],
|
|
matchesContextOverflowError: ({ errorMessage }) =>
|
|
/\bcontent_filter\b.*\btoo long\b/i.test(errorMessage),
|
|
classifyFailoverReason: ({ errorMessage }) =>
|
|
/\bquota exceeded\b/i.test(errorMessage) ? "rate_limit" : undefined,
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
matchesProviderContextOverflowWithPlugin({
|
|
provider: "azure-openai-responses",
|
|
context: {
|
|
provider: "azure-openai-responses",
|
|
errorMessage: "content_filter prompt too long",
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
classifyProviderFailoverReasonWithPlugin({
|
|
provider: "azure-openai-responses",
|
|
context: {
|
|
provider: "azure-openai-responses",
|
|
errorMessage: "quota exceeded",
|
|
},
|
|
}),
|
|
).toBe("rate_limit");
|
|
});
|
|
|
|
it("resolves stream wrapper hooks through hook-only aliases without provider ownership", () => {
|
|
const wrappedStreamFn = vi.fn();
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
hookAliases: ["azure-openai-responses"],
|
|
auth: [],
|
|
wrapStreamFn: ({ streamFn }) => streamFn ?? wrappedStreamFn,
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
wrapProviderStreamFn({
|
|
provider: "azure-openai-responses",
|
|
context: createDemoResolvedModelContext({
|
|
provider: "azure-openai-responses",
|
|
streamFn: wrappedStreamFn,
|
|
}),
|
|
}),
|
|
).toBe(wrappedStreamFn);
|
|
});
|
|
|
|
it("normalizes transport hooks without needing provider ownership", () => {
|
|
resolvePluginProvidersMock.mockReturnValue([
|
|
{
|
|
id: "google",
|
|
label: "Google",
|
|
auth: [],
|
|
normalizeTransport: ({ api, baseUrl }) =>
|
|
api === "google-generative-ai" && baseUrl === "https://generativelanguage.googleapis.com"
|
|
? {
|
|
api,
|
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
}
|
|
: undefined,
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
normalizeProviderTransportWithPlugin({
|
|
provider: "google-paid",
|
|
context: {
|
|
provider: "google-paid",
|
|
api: "google-generative-ai",
|
|
baseUrl: "https://generativelanguage.googleapis.com",
|
|
},
|
|
}),
|
|
).toEqual({
|
|
api: "google-generative-ai",
|
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
});
|
|
});
|
|
|
|
it("invalidates cached runtime providers when config mutates in place", () => {
|
|
const config = {
|
|
plugins: {
|
|
entries: {
|
|
demo: { enabled: false },
|
|
},
|
|
},
|
|
} as { plugins: { entries: { demo: { enabled: boolean } } } };
|
|
resolvePluginProvidersMock.mockImplementation((params) => {
|
|
const runtimeConfig = params?.config as typeof config | undefined;
|
|
const enabled = runtimeConfig?.plugins?.entries?.demo?.enabled === true;
|
|
return enabled
|
|
? [
|
|
{
|
|
id: DEMO_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
},
|
|
]
|
|
: [];
|
|
});
|
|
|
|
expect(
|
|
resolveProviderRuntimePlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
config: config as never,
|
|
}),
|
|
).toBeUndefined();
|
|
|
|
config.plugins.entries.demo.enabled = true;
|
|
|
|
expect(
|
|
resolveProviderRuntimePlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
config: config as never,
|
|
}),
|
|
).toMatchObject({
|
|
id: DEMO_PROVIDER_ID,
|
|
});
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("dispatches runtime hooks for the matched provider", async () => {
|
|
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]);
|
|
resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue(["demo"]);
|
|
const prepareDynamicModel = vi.fn(async () => undefined);
|
|
const createStreamFn = vi.fn(() => vi.fn());
|
|
const createEmbeddingProvider = vi.fn(async () => ({
|
|
id: "demo",
|
|
model: "demo-embed",
|
|
embedQuery: async () => [1, 0, 0],
|
|
embedBatch: async () => [[1, 0, 0]],
|
|
client: { token: "embed-token" },
|
|
}));
|
|
const buildReplayPolicy = vi.fn(() => ({
|
|
sanitizeMode: "full" as const,
|
|
toolCallIdMode: "strict9" as const,
|
|
allowSyntheticToolResults: true,
|
|
}));
|
|
const sanitizeReplayHistory = vi.fn(
|
|
async ({
|
|
messages,
|
|
}: Pick<ProviderSanitizeReplayHistoryContext, "messages">): Promise<AgentMessage[]> => [
|
|
...messages,
|
|
DEMO_SANITIZED_MESSAGE,
|
|
],
|
|
);
|
|
const validateReplayTurns = vi.fn(
|
|
async ({
|
|
messages,
|
|
}: Pick<ProviderValidateReplayTurnsContext, "messages">): Promise<AgentMessage[]> => messages,
|
|
);
|
|
const normalizeToolSchemas = vi.fn(
|
|
({ tools }: Pick<ProviderNormalizeToolSchemasContext, "tools">): AnyAgentTool[] => tools,
|
|
);
|
|
const inspectToolSchemas = vi.fn(() => [] as { toolName: string; violations: string[] }[]);
|
|
const resolveReasoningOutputMode = vi.fn(() => "tagged" as const);
|
|
const resolveSyntheticAuth = vi.fn(() => ({
|
|
apiKey: "demo-local",
|
|
source: "models.providers.demo (synthetic local key)",
|
|
mode: "api-key" as const,
|
|
}));
|
|
const shouldDeferSyntheticProfileAuth = vi.fn(
|
|
({ resolvedApiKey }: { resolvedApiKey?: string }) => resolvedApiKey === "demo-local",
|
|
);
|
|
const buildUnknownModelHint = vi.fn(
|
|
({ modelId }: { modelId: string }) => `Use demo setup for ${modelId}`,
|
|
);
|
|
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_PROVIDER_ID,
|
|
label: "Demo",
|
|
auth: [],
|
|
normalizeConfig: ({ providerConfig }) => ({
|
|
...providerConfig,
|
|
baseUrl: "https://normalized.example.com/v1",
|
|
}),
|
|
normalizeTransport: ({ api, baseUrl }) => ({
|
|
api,
|
|
baseUrl: baseUrl ? `${baseUrl}/normalized` : undefined,
|
|
}),
|
|
normalizeModelId: ({ modelId }) => modelId.replace("-legacy", ""),
|
|
resolveDynamicModel: () => MODEL,
|
|
prepareDynamicModel,
|
|
applyNativeStreamingUsageCompat: ({ providerConfig }) => ({
|
|
...providerConfig,
|
|
compat: { supportsUsageInStreaming: true },
|
|
}),
|
|
buildReplayPolicy,
|
|
sanitizeReplayHistory,
|
|
validateReplayTurns,
|
|
normalizeToolSchemas,
|
|
inspectToolSchemas,
|
|
resolveReasoningOutputMode,
|
|
prepareExtraParams: ({ extraParams }) => ({
|
|
...extraParams,
|
|
transport: "auto",
|
|
}),
|
|
createStreamFn,
|
|
wrapStreamFn: ({ streamFn, model }) => {
|
|
expect(model).toMatchObject(MODEL);
|
|
return streamFn;
|
|
},
|
|
createEmbeddingProvider,
|
|
resolveSyntheticAuth,
|
|
resolveExternalAuthProfiles: ({ store }): ProviderExternalAuthProfile[] =>
|
|
store.profiles["demo:managed"]
|
|
? []
|
|
: [
|
|
{
|
|
persistence: "runtime-only",
|
|
profileId: "demo:managed",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: DEMO_PROVIDER_ID,
|
|
access: "external-access",
|
|
refresh: "external-refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
},
|
|
],
|
|
shouldDeferSyntheticProfileAuth,
|
|
normalizeResolvedModel: ({ model }) => ({
|
|
...model,
|
|
api: "openai-codex-responses",
|
|
}),
|
|
formatApiKey: (cred) =>
|
|
cred.type === "oauth" ? JSON.stringify({ token: cred.access }) : "",
|
|
refreshOAuth,
|
|
resolveConfigApiKey: () => "DEMO_PROFILE",
|
|
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"),
|
|
},
|
|
{
|
|
...createOpenAiCatalogProviderPlugin({
|
|
buildMissingAuthMessage: () =>
|
|
'No API key found for provider "openai". Use openai/gpt-5.5.',
|
|
buildUnknownModelHint,
|
|
}),
|
|
} as ProviderPlugin,
|
|
];
|
|
});
|
|
|
|
expect(
|
|
runProviderDynamicModel({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoRuntimeContext({
|
|
modelRegistry: EMPTY_MODEL_REGISTRY,
|
|
}),
|
|
}),
|
|
).toMatchObject(MODEL);
|
|
|
|
expect(
|
|
normalizeProviderModelIdWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
modelId: "demo-model-legacy",
|
|
},
|
|
}),
|
|
).toBe("demo-model");
|
|
|
|
expect(
|
|
normalizeProviderTransportWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
api: "openai-completions",
|
|
baseUrl: "https://demo.example.com",
|
|
},
|
|
}),
|
|
).toEqual({
|
|
api: "openai-completions",
|
|
baseUrl: "https://demo.example.com/normalized",
|
|
});
|
|
|
|
expect(
|
|
normalizeProviderConfigWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
providerConfig: {
|
|
baseUrl: "https://demo.example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
},
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
baseUrl: "https://normalized.example.com/v1",
|
|
});
|
|
|
|
expect(
|
|
applyProviderNativeStreamingUsageCompatWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
providerConfig: {
|
|
baseUrl: "https://demo.example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
},
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
compat: { supportsUsageInStreaming: true },
|
|
});
|
|
|
|
expect(
|
|
resolveProviderConfigApiKeyWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
env: { DEMO_PROFILE: "default" } as NodeJS.ProcessEnv,
|
|
},
|
|
}),
|
|
).toBe("DEMO_PROFILE");
|
|
|
|
await prepareProviderDynamicModel({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoRuntimeContext({
|
|
modelRegistry: EMPTY_MODEL_REGISTRY,
|
|
}),
|
|
});
|
|
|
|
expect(
|
|
resolveProviderReplayPolicyWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
modelApi: MODEL.api,
|
|
}),
|
|
}),
|
|
).toMatchObject({
|
|
sanitizeMode: "full",
|
|
toolCallIdMode: "strict9",
|
|
allowSyntheticToolResults: true,
|
|
});
|
|
|
|
expect(
|
|
resolveProviderReasoningOutputModeWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
modelApi: MODEL.api,
|
|
}),
|
|
}),
|
|
).toBe("tagged");
|
|
|
|
expect(
|
|
prepareProviderExtraParams({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoRuntimeContext({
|
|
extraParams: { temperature: 0.3 },
|
|
}),
|
|
}),
|
|
).toMatchObject({
|
|
temperature: 0.3,
|
|
transport: "auto",
|
|
});
|
|
|
|
expect(
|
|
resolveProviderStreamFn({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({}),
|
|
}),
|
|
).toBeTypeOf("function");
|
|
|
|
await expectResolvedMatches([
|
|
{
|
|
actual: () =>
|
|
createProviderEmbeddingProvider({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
config: {} as never,
|
|
model: "demo-embed",
|
|
}),
|
|
}),
|
|
expected: {
|
|
id: "demo",
|
|
model: "demo-embed",
|
|
client: { token: "embed-token" },
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
prepareProviderRuntimeAuth({
|
|
provider: DEMO_PROVIDER_ID,
|
|
env: process.env,
|
|
context: createDemoResolvedModelContext({
|
|
env: process.env,
|
|
apiKey: "source-token",
|
|
authMode: "api-key",
|
|
}),
|
|
}),
|
|
expected: {
|
|
apiKey: "runtime-token",
|
|
baseUrl: "https://runtime.example.com/v1",
|
|
expiresAt: 123,
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
refreshProviderOAuthCredentialWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
type: "oauth",
|
|
access: "oauth-access",
|
|
refresh: "oauth-refresh",
|
|
expires: Date.now() + 60_000,
|
|
}),
|
|
}),
|
|
expected: {
|
|
access: "refreshed-access-token",
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderUsageAuthWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
env: process.env,
|
|
context: createDemoProviderContext({
|
|
config: {} as never,
|
|
env: process.env,
|
|
resolveApiKeyFromConfigAndStore: () => "source-token",
|
|
resolveOAuthToken: async () => null,
|
|
}),
|
|
}),
|
|
expected: {
|
|
token: "usage-token",
|
|
accountId: "usage-account",
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderUsageSnapshotWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
env: process.env,
|
|
context: createDemoProviderContext({
|
|
config: {} as never,
|
|
env: process.env,
|
|
token: "usage-token",
|
|
timeoutMs: 5_000,
|
|
fetchFn: vi.fn() as never,
|
|
}),
|
|
}),
|
|
expected: {
|
|
provider: "zai",
|
|
windows: [{ label: "Day", usedPercent: 25 }],
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
sanitizeProviderReplayHistoryWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
modelApi: MODEL.api,
|
|
sessionId: "session-1",
|
|
messages: DEMO_REPLAY_MESSAGES,
|
|
}),
|
|
}),
|
|
expected: {
|
|
1: DEMO_SANITIZED_MESSAGE,
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
validateProviderReplayTurnsWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
modelApi: MODEL.api,
|
|
sessionId: "session-1",
|
|
messages: DEMO_REPLAY_MESSAGES,
|
|
}),
|
|
}),
|
|
expected: {
|
|
0: DEMO_REPLAY_MESSAGES[0],
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
wrapProviderStreamFn({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
streamFn: vi.fn(),
|
|
}),
|
|
}),
|
|
).toBeTypeOf("function");
|
|
|
|
expect(
|
|
normalizeProviderToolSchemasWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
modelApi: MODEL.api,
|
|
tools: [DEMO_TOOL],
|
|
}),
|
|
}),
|
|
).toEqual([DEMO_TOOL]);
|
|
|
|
expect(
|
|
inspectProviderToolSchemasWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({
|
|
modelApi: MODEL.api,
|
|
tools: [DEMO_TOOL],
|
|
}),
|
|
}),
|
|
).toEqual([]);
|
|
|
|
expect(
|
|
normalizeProviderResolvedModelWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({}),
|
|
}),
|
|
).toMatchObject({
|
|
...MODEL,
|
|
api: "openai-codex-responses",
|
|
});
|
|
|
|
expect(
|
|
applyProviderResolvedModelCompatWithPlugins({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoResolvedModelContext({}),
|
|
}),
|
|
).toBeUndefined();
|
|
|
|
expect(
|
|
formatProviderAuthProfileApiKeyWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
type: "oauth",
|
|
provider: DEMO_PROVIDER_ID,
|
|
access: "oauth-access",
|
|
refresh: "oauth-refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
}),
|
|
).toBe('{"token":"oauth-access"}');
|
|
|
|
await expectResolvedAsyncValues([
|
|
{
|
|
actual: () =>
|
|
buildProviderAuthDoctorHintWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
profileId: "demo:default",
|
|
store: { version: 1, profiles: {} },
|
|
}),
|
|
}),
|
|
expected: "Repair demo:default",
|
|
},
|
|
]);
|
|
|
|
expectResolvedValues([
|
|
{
|
|
actual: () =>
|
|
resolveProviderCacheTtlEligibility({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
modelId: "anthropic/claude-sonnet-4-6",
|
|
}),
|
|
}),
|
|
expected: true,
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderBinaryThinking({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
modelId: "glm-5",
|
|
}),
|
|
}),
|
|
expected: true,
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderXHighThinking({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
modelId: "gpt-5.4",
|
|
}),
|
|
}),
|
|
expected: true,
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderDefaultThinkingLevel({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
modelId: "gpt-5.4",
|
|
reasoning: true,
|
|
}),
|
|
}),
|
|
expected: "low",
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderModernModelRef({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
modelId: "gpt-5.4",
|
|
}),
|
|
}),
|
|
expected: true,
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveExternalAuthProfilesWithPlugins({
|
|
env: process.env,
|
|
context: {
|
|
env: process.env,
|
|
store: { version: 1, profiles: {} },
|
|
},
|
|
}),
|
|
expected: [
|
|
{
|
|
persistence: "runtime-only",
|
|
profileId: "demo:managed",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: DEMO_PROVIDER_ID,
|
|
access: "external-access",
|
|
refresh: "external-refresh",
|
|
expires: expect.any(Number),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
actual: () =>
|
|
resolveProviderSyntheticAuthWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: createDemoProviderContext({
|
|
providerConfig: {
|
|
api: "openai-completions",
|
|
baseUrl: "http://localhost:11434",
|
|
models: [],
|
|
},
|
|
}),
|
|
}),
|
|
expected: {
|
|
apiKey: "demo-local",
|
|
source: "models.providers.demo (synthetic local key)",
|
|
mode: "api-key",
|
|
},
|
|
},
|
|
{
|
|
actual: () =>
|
|
shouldDeferProviderSyntheticProfileAuthWithPlugin({
|
|
provider: DEMO_PROVIDER_ID,
|
|
context: {
|
|
provider: DEMO_PROVIDER_ID,
|
|
resolvedApiKey: "demo-local",
|
|
},
|
|
}),
|
|
expected: true,
|
|
},
|
|
{
|
|
actual: () =>
|
|
buildProviderUnknownModelHintWithPlugin({
|
|
provider: "openai",
|
|
env: process.env,
|
|
context: {
|
|
env: process.env,
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
},
|
|
}),
|
|
expected: "Use demo setup for gpt-5.4",
|
|
},
|
|
]);
|
|
|
|
expectCodexMissingAuthHint(buildProviderMissingAuthMessageWithPlugin);
|
|
await expectAugmentedCodexCatalog(augmentModelCatalogWithProviderPlugins);
|
|
|
|
expectCalledOnce(
|
|
buildReplayPolicy,
|
|
prepareDynamicModel,
|
|
sanitizeReplayHistory,
|
|
validateReplayTurns,
|
|
normalizeToolSchemas,
|
|
inspectToolSchemas,
|
|
resolveReasoningOutputMode,
|
|
refreshOAuth,
|
|
resolveSyntheticAuth,
|
|
shouldDeferSyntheticProfileAuth,
|
|
buildUnknownModelHint,
|
|
prepareRuntimeAuth,
|
|
resolveUsageAuth,
|
|
fetchUsageSnapshot,
|
|
);
|
|
});
|
|
|
|
it("matches provider hooks through a custom provider's native api owner", () => {
|
|
const ollamaPlugin: ProviderPlugin = {
|
|
id: "ollama",
|
|
label: "Ollama",
|
|
auth: [],
|
|
createStreamFn: vi.fn(() => vi.fn()),
|
|
};
|
|
resolvePluginProvidersMock.mockReturnValue([ollamaPlugin]);
|
|
|
|
const plugin = resolveProviderRuntimePlugin({
|
|
provider: "ollama-spark",
|
|
config: {
|
|
models: {
|
|
providers: {
|
|
"ollama-spark": {
|
|
api: "ollama",
|
|
baseUrl: "http://127.0.0.1:11434",
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
expect(plugin).toBe(ollamaPlugin);
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
providerRefs: ["ollama-spark", "ollama"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not match alias hooks when an exact custom provider declares a foreign api owner", () => {
|
|
const qwenPlugin: ProviderPlugin = {
|
|
id: "qwen",
|
|
label: "Qwen",
|
|
aliases: ["modelstudio"],
|
|
auth: [],
|
|
createStreamFn: vi.fn(() => vi.fn()),
|
|
};
|
|
resolvePluginProvidersMock.mockReturnValue([qwenPlugin]);
|
|
|
|
const plugin = resolveProviderRuntimePlugin({
|
|
provider: "modelstudio",
|
|
config: {
|
|
models: {
|
|
providers: {
|
|
modelstudio: {
|
|
api: "openai-completions",
|
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
expect(plugin).toBeUndefined();
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
providerRefs: ["modelstudio", "openai-completions"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("merges compat contributions from owner and foreign provider plugins", () => {
|
|
resolvePluginProvidersMock.mockImplementation((params) => {
|
|
const onlyPluginIds = params.onlyPluginIds ?? [];
|
|
const plugins: ProviderPlugin[] = [
|
|
{
|
|
id: "openrouter",
|
|
label: "OpenRouter",
|
|
auth: [],
|
|
contributeResolvedModelCompat: () => ({ supportsStrictMode: true }),
|
|
},
|
|
{
|
|
id: "mistral",
|
|
label: "Mistral",
|
|
auth: [],
|
|
contributeResolvedModelCompat: ({ modelId }) =>
|
|
modelId.startsWith("mistralai/") ? { supportsStore: false } : undefined,
|
|
},
|
|
];
|
|
return onlyPluginIds.length > 0
|
|
? plugins.filter((plugin) => onlyPluginIds.includes(plugin.id))
|
|
: plugins;
|
|
});
|
|
|
|
expect(
|
|
applyProviderResolvedModelCompatWithPlugins({
|
|
provider: "openrouter",
|
|
context: createDemoResolvedModelContext({
|
|
provider: "openrouter",
|
|
modelId: "mistralai/mistral-small-3.2-24b-instruct",
|
|
model: {
|
|
...MODEL,
|
|
provider: "openrouter",
|
|
id: "mistralai/mistral-small-3.2-24b-instruct",
|
|
compat: { supportsDeveloperRole: false },
|
|
},
|
|
}),
|
|
}),
|
|
).toMatchObject({
|
|
compat: {
|
|
supportsDeveloperRole: false,
|
|
supportsStrictMode: true,
|
|
supportsStore: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("applies foreign transport normalization for custom provider hosts", () => {
|
|
resolvePluginProvidersMock.mockImplementation((params) => {
|
|
const onlyPluginIds = params.onlyPluginIds ?? [];
|
|
const plugins: ProviderPlugin[] = [
|
|
{
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
auth: [],
|
|
normalizeTransport: ({ provider, api, baseUrl }) =>
|
|
provider === "custom-openai" &&
|
|
api === "openai-completions" &&
|
|
baseUrl === "https://api.openai.com/v1"
|
|
? { api: "openai-responses", baseUrl }
|
|
: undefined,
|
|
},
|
|
];
|
|
return onlyPluginIds.length > 0
|
|
? plugins.filter((plugin) => onlyPluginIds.includes(plugin.id))
|
|
: plugins;
|
|
});
|
|
|
|
expect(
|
|
applyProviderResolvedTransportWithPlugin({
|
|
provider: "custom-openai",
|
|
context: createDemoResolvedModelContext({
|
|
provider: "custom-openai",
|
|
modelId: "gpt-5.4",
|
|
model: {
|
|
...MODEL,
|
|
provider: "custom-openai",
|
|
id: "gpt-5.4",
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
},
|
|
}),
|
|
}),
|
|
).toMatchObject({
|
|
provider: "custom-openai",
|
|
id: "gpt-5.4",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
});
|
|
});
|
|
|
|
it("resolves bundled catalog hooks through provider plugins", async () => {
|
|
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]);
|
|
resolvePluginProvidersMock.mockImplementation((params?: { onlyPluginIds?: string[] }) => {
|
|
const onlyPluginIds = params?.onlyPluginIds;
|
|
if (!onlyPluginIds || !onlyPluginIds.includes("openai")) {
|
|
return [];
|
|
}
|
|
return [createOpenAiCatalogProviderPlugin()];
|
|
});
|
|
|
|
await expect(
|
|
augmentModelCatalogWithProviderPlugins({
|
|
env: process.env,
|
|
context: {
|
|
env: process.env,
|
|
entries: [
|
|
{ provider: "openai", id: "gpt-5.4", name: "GPT-5.2" },
|
|
{ provider: "openai", id: "gpt-5.4-pro", name: "GPT-5.2 Pro" },
|
|
{ provider: "openai", id: "gpt-5.4-mini", name: "GPT-5 mini" },
|
|
{ provider: "openai", id: "gpt-5.4-nano", name: "GPT-5 nano" },
|
|
{ provider: "openai-codex", id: "gpt-5.4", name: "GPT-5.4" },
|
|
],
|
|
},
|
|
}),
|
|
).resolves.toEqual(expectedAugmentedOpenaiCodexCatalogEntries);
|
|
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
onlyPluginIds: ["openai"],
|
|
activate: false,
|
|
cache: false,
|
|
}),
|
|
);
|
|
expect(resolveCatalogHookProviderPluginIdsMock).toHaveBeenCalledTimes(1);
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not stack-overflow when provider hook resolution reenters the same plugin load", () => {
|
|
let providerLoadInFlight = false;
|
|
isPluginProvidersLoadInFlightMock.mockImplementation(() => providerLoadInFlight);
|
|
resolvePluginProvidersMock.mockImplementation(() => {
|
|
providerLoadInFlight = true;
|
|
try {
|
|
const reentrantResult = normalizeProviderConfigWithPlugin({
|
|
provider: "reentrant-provider",
|
|
context: {
|
|
provider: "reentrant-provider",
|
|
providerConfig: {
|
|
baseUrl: "https://example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
},
|
|
},
|
|
});
|
|
expect(reentrantResult).toBeUndefined();
|
|
return [];
|
|
} finally {
|
|
providerLoadInFlight = false;
|
|
}
|
|
});
|
|
|
|
const result = normalizeProviderConfigWithPlugin({
|
|
provider: "demo",
|
|
context: {
|
|
provider: "demo",
|
|
providerConfig: { baseUrl: "https://example.com", api: "openai-completions", models: [] },
|
|
},
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("keeps cached provider hook results available during a nested provider load", () => {
|
|
const cachedNormalizedConfig: ModelProviderConfig = {
|
|
baseUrl: "https://cached.example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
};
|
|
let providerLoadInFlight = false;
|
|
isPluginProvidersLoadInFlightMock.mockImplementation(() => providerLoadInFlight);
|
|
resolvePluginProvidersMock.mockImplementation((params) => {
|
|
const providerRef = params?.providerRefs?.[0];
|
|
if (providerRef === "cached-provider") {
|
|
return [
|
|
{
|
|
id: "cached-provider",
|
|
label: "Cached Provider",
|
|
auth: [],
|
|
normalizeConfig: () => cachedNormalizedConfig,
|
|
},
|
|
];
|
|
}
|
|
providerLoadInFlight = true;
|
|
try {
|
|
const reentrantResult = normalizeProviderConfigWithPlugin({
|
|
provider: "cached-provider",
|
|
context: {
|
|
provider: "cached-provider",
|
|
providerConfig: {
|
|
baseUrl: "https://example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
},
|
|
},
|
|
});
|
|
expect(reentrantResult).toBe(cachedNormalizedConfig);
|
|
return [];
|
|
} finally {
|
|
providerLoadInFlight = false;
|
|
}
|
|
});
|
|
|
|
expect(
|
|
normalizeProviderConfigWithPlugin({
|
|
provider: "cached-provider",
|
|
context: {
|
|
provider: "cached-provider",
|
|
providerConfig: { baseUrl: "https://example.com", api: "openai-completions", models: [] },
|
|
},
|
|
}),
|
|
).toBe(cachedNormalizedConfig);
|
|
|
|
expect(
|
|
normalizeProviderConfigWithPlugin({
|
|
provider: "outer-provider",
|
|
context: {
|
|
provider: "outer-provider",
|
|
providerConfig: {
|
|
baseUrl: "https://outer.example.com",
|
|
api: "openai-completions",
|
|
models: [],
|
|
},
|
|
},
|
|
}),
|
|
).toBeUndefined();
|
|
|
|
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|