openclaw/src/agents/provider-capabilities.test.ts

227 lines
8.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveProviderCapabilitiesWithPluginMock = vi.fn((params: { provider: string }) => {
switch (params.provider) {
case "anthropic":
return {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
};
case "openai":
return {
providerFamily: "openai",
};
case "openrouter":
return {
openAiCompatTurnValidation: false,
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
};
case "openai-codex":
return {
providerFamily: "openai",
};
case "github-copilot":
return {
dropThinkingBlockModelHints: ["claude"],
};
case "kilocode":
return {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
};
case "kimi":
return {
openAiPayloadNormalizationMode: "moonshot-thinking",
preserveAnthropicThinkingSignatures: false,
};
default:
return undefined;
}
});
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderCapabilitiesWithPlugin: (params: { provider: string }) =>
resolveProviderCapabilitiesWithPluginMock(params),
}));
let isAnthropicProviderFamily: typeof import("./provider-capabilities.js").isAnthropicProviderFamily;
let isOpenAiProviderFamily: typeof import("./provider-capabilities.js").isOpenAiProviderFamily;
let requiresOpenAiCompatibleAnthropicToolPayload: typeof import("./provider-capabilities.js").requiresOpenAiCompatibleAnthropicToolPayload;
let resolveProviderCapabilities: typeof import("./provider-capabilities.js").resolveProviderCapabilities;
let resolveTranscriptToolCallIdMode: typeof import("./provider-capabilities.js").resolveTranscriptToolCallIdMode;
let shouldDropThinkingBlocksForModel: typeof import("./provider-capabilities.js").shouldDropThinkingBlocksForModel;
let shouldSanitizeGeminiThoughtSignaturesForModel: typeof import("./provider-capabilities.js").shouldSanitizeGeminiThoughtSignaturesForModel;
let supportsOpenAiCompatTurnValidation: typeof import("./provider-capabilities.js").supportsOpenAiCompatTurnValidation;
let usesMoonshotThinkingPayloadCompat: typeof import("./provider-capabilities.js").usesMoonshotThinkingPayloadCompat;
async function loadFreshProviderCapabilitiesModuleForTest() {
vi.resetModules();
({
isAnthropicProviderFamily,
isOpenAiProviderFamily,
requiresOpenAiCompatibleAnthropicToolPayload,
resolveProviderCapabilities,
resolveTranscriptToolCallIdMode,
shouldDropThinkingBlocksForModel,
shouldSanitizeGeminiThoughtSignaturesForModel,
supportsOpenAiCompatTurnValidation,
usesMoonshotThinkingPayloadCompat,
} = await import("./provider-capabilities.js"));
}
describe("resolveProviderCapabilities", () => {
beforeEach(async () => {
await loadFreshProviderCapabilitiesModuleForTest();
resolveProviderCapabilitiesWithPluginMock.mockClear();
});
it("returns provider-owned anthropic defaults for ordinary providers", () => {
expect(resolveProviderCapabilities("anthropic")).toEqual({
anthropicToolSchemaMode: "native",
anthropicToolChoiceMode: "native",
openAiPayloadNormalizationMode: "default",
providerFamily: "anthropic",
preserveAnthropicThinkingSignatures: true,
openAiCompatTurnValidation: true,
geminiThoughtSignatureSanitization: false,
transcriptToolCallIdMode: "default",
transcriptToolCallIdModelHints: [],
geminiThoughtSignatureModelHints: [],
dropThinkingBlockModelHints: ["claude"],
});
expect(resolveProviderCapabilities("anthropic-vertex")).toEqual({
anthropicToolSchemaMode: "native",
anthropicToolChoiceMode: "native",
openAiPayloadNormalizationMode: "default",
providerFamily: "anthropic",
preserveAnthropicThinkingSignatures: true,
openAiCompatTurnValidation: true,
geminiThoughtSignatureSanitization: false,
transcriptToolCallIdMode: "default",
transcriptToolCallIdModelHints: [],
geminiThoughtSignatureModelHints: [],
dropThinkingBlockModelHints: ["claude"],
});
expect(resolveProviderCapabilities("amazon-bedrock")).toEqual({
anthropicToolSchemaMode: "native",
anthropicToolChoiceMode: "native",
openAiPayloadNormalizationMode: "default",
providerFamily: "anthropic",
preserveAnthropicThinkingSignatures: true,
openAiCompatTurnValidation: true,
geminiThoughtSignatureSanitization: false,
transcriptToolCallIdMode: "default",
transcriptToolCallIdModelHints: [],
geminiThoughtSignatureModelHints: [],
dropThinkingBlockModelHints: ["claude"],
});
});
it("normalizes kimi aliases to the same capability set", () => {
expect(resolveProviderCapabilities("kimi")).toEqual(resolveProviderCapabilities("kimi-code"));
expect(resolveProviderCapabilities("kimi-code")).toEqual({
anthropicToolSchemaMode: "native",
anthropicToolChoiceMode: "native",
openAiPayloadNormalizationMode: "moonshot-thinking",
providerFamily: "default",
preserveAnthropicThinkingSignatures: false,
openAiCompatTurnValidation: true,
geminiThoughtSignatureSanitization: false,
transcriptToolCallIdMode: "default",
transcriptToolCallIdModelHints: [],
geminiThoughtSignatureModelHints: [],
dropThinkingBlockModelHints: [],
});
});
it("flags providers that opt out of OpenAI-compatible turn validation", () => {
expect(supportsOpenAiCompatTurnValidation("openrouter")).toBe(false);
expect(supportsOpenAiCompatTurnValidation("opencode")).toBe(false);
expect(supportsOpenAiCompatTurnValidation("opencode-go")).toBe(false);
expect(supportsOpenAiCompatTurnValidation("moonshot")).toBe(true);
});
it("routes moonshot payload compatibility through the capability registry", () => {
expect(usesMoonshotThinkingPayloadCompat("moonshot")).toBe(true);
expect(usesMoonshotThinkingPayloadCompat("kimi-coding")).toBe(true);
expect(usesMoonshotThinkingPayloadCompat("openai")).toBe(false);
});
it("resolves transcript thought-signature and tool-call quirks through the registry", () => {
expect(
shouldSanitizeGeminiThoughtSignaturesForModel({
provider: "openrouter",
modelId: "google/gemini-2.5-pro-preview",
}),
).toBe(true);
expect(
shouldSanitizeGeminiThoughtSignaturesForModel({
provider: "kilocode",
modelId: "gemini-2.0-flash",
}),
).toBe(true);
expect(
shouldSanitizeGeminiThoughtSignaturesForModel({
provider: "opencode-go",
modelId: "google/gemini-2.5-pro-preview",
}),
).toBe(true);
expect(resolveTranscriptToolCallIdMode("mistral", "mistral-large-latest")).toBe("strict9");
});
it("treats kimi aliases as native anthropic tool payload providers", () => {
expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi")).toBe(false);
expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(false);
expect(requiresOpenAiCompatibleAnthropicToolPayload("anthropic")).toBe(false);
});
it("tracks provider families and model-specific transcript quirks in the registry", () => {
expect(isOpenAiProviderFamily("openai")).toBe(true);
expect(isAnthropicProviderFamily("anthropic-vertex")).toBe(true);
expect(isAnthropicProviderFamily("amazon-bedrock")).toBe(true);
expect(
shouldDropThinkingBlocksForModel({
provider: "anthropic",
modelId: "claude-opus-4-6",
}),
).toBe(true);
expect(
shouldDropThinkingBlocksForModel({
provider: "anthropic-vertex",
modelId: "claude-sonnet-4-6",
}),
).toBe(true);
expect(
shouldDropThinkingBlocksForModel({
provider: "amazon-bedrock",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
}),
).toBe(true);
expect(
shouldDropThinkingBlocksForModel({
provider: "github-copilot",
modelId: "claude-3.7-sonnet",
}),
).toBe(true);
});
it("forwards config and workspace context to plugin capability lookup", () => {
const config = { plugins: { enabled: true } };
const env = { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv;
resolveProviderCapabilities("anthropic", {
config,
workspaceDir: "/tmp/workspace",
env,
});
expect(resolveProviderCapabilitiesWithPluginMock).toHaveBeenLastCalledWith({
provider: "anthropic",
config,
workspaceDir: "/tmp/workspace",
env,
});
});
});