mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 23:00:22 +00:00
refactor: move bundled replay policy ownership into plugins (#60452)
* refactor: move bundled replay policy ownership into plugins * test: preserve replay fallback until providers adopt hooks * test: cover response replay branches for ollama and zai --------- Co-authored-by: Shakker <shakkerdroid@gmail.com>
This commit is contained in:
@@ -11,16 +11,27 @@ vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
"kilocode",
|
||||
"kimi",
|
||||
"kimi-code",
|
||||
"minimax",
|
||||
"minimax-portal",
|
||||
"mistral",
|
||||
"moonshot",
|
||||
"openai",
|
||||
"openai-codex",
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"ollama",
|
||||
"openrouter",
|
||||
"sglang",
|
||||
"vllm",
|
||||
"xai",
|
||||
"zai",
|
||||
].includes(provider)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (provider === "sglang" || provider === "vllm") {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
buildReplayPolicy: (context?: { modelId?: string; modelApi?: string }) => {
|
||||
const modelId = context?.modelId?.toLowerCase() ?? "";
|
||||
@@ -37,6 +48,38 @@ vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
allowSyntheticToolResults: true,
|
||||
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
|
||||
};
|
||||
case "minimax":
|
||||
case "minimax-portal":
|
||||
return context?.modelApi === "openai-completions"
|
||||
? {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
}
|
||||
: {
|
||||
sanitizeMode: "full",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
validateAnthropicTurns: true,
|
||||
allowSyntheticToolResults: true,
|
||||
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
|
||||
};
|
||||
case "moonshot":
|
||||
case "ollama":
|
||||
case "zai":
|
||||
return context?.modelApi === "openai-completions"
|
||||
? {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
}
|
||||
: undefined;
|
||||
case "google":
|
||||
return {
|
||||
sanitizeMode: "full",
|
||||
@@ -88,6 +131,28 @@ vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
case "xai":
|
||||
if (
|
||||
context?.modelApi === "openai-completions" ||
|
||||
context?.modelApi === "openai-responses"
|
||||
) {
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
...(context.modelApi === "openai-completions"
|
||||
? {
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
}
|
||||
: {
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
case "kilocode":
|
||||
return modelId.includes("gemini")
|
||||
? {
|
||||
@@ -183,10 +248,10 @@ describe("resolveTranscriptPolicy", () => {
|
||||
expect(policy.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to transport defaults when a plugin replay hook returns undefined", () => {
|
||||
it("falls back to unowned transport defaults when no owning plugin exists", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "kilocode",
|
||||
modelId: "kilocode-default",
|
||||
provider: "custom-openai-proxy",
|
||||
modelId: "demo-model",
|
||||
modelApi: "openai-completions",
|
||||
});
|
||||
|
||||
@@ -197,6 +262,49 @@ describe("resolveTranscriptPolicy", () => {
|
||||
expect(policy.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves transport defaults when a runtime plugin has not adopted replay hooks", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "vllm",
|
||||
modelId: "demo-model",
|
||||
modelApi: "openai-completions",
|
||||
});
|
||||
|
||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||
expect(policy.toolCallIdMode).toBe("strict");
|
||||
expect(policy.applyGoogleTurnOrdering).toBe(true);
|
||||
expect(policy.validateGeminiTurns).toBe(true);
|
||||
expect(policy.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
it("uses provider-owned Anthropic replay policy for MiniMax transports", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "minimax",
|
||||
modelId: "MiniMax-M2.7",
|
||||
modelApi: "anthropic-messages",
|
||||
});
|
||||
|
||||
expect(policy.sanitizeMode).toBe("full");
|
||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||
expect(policy.preserveSignatures).toBe(true);
|
||||
expect(policy.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
it("uses provider-owned OpenAI-compatible replay policy for MiniMax portal completions", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "minimax-portal",
|
||||
modelId: "MiniMax-M2.7",
|
||||
modelApi: "openai-completions",
|
||||
});
|
||||
|
||||
expect(policy.sanitizeMode).toBe("images-only");
|
||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||
expect(policy.toolCallIdMode).toBe("strict");
|
||||
expect(policy.preserveSignatures).toBe(false);
|
||||
expect(policy.applyGoogleTurnOrdering).toBe(true);
|
||||
expect(policy.validateGeminiTurns).toBe(true);
|
||||
expect(policy.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
it("enables Anthropic-compatible policies for Bedrock provider", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "amazon-bedrock",
|
||||
|
||||
@@ -44,7 +44,14 @@ function isAnthropicApi(modelApi?: string | null): boolean {
|
||||
return modelApi === "anthropic-messages" || modelApi === "bedrock-converse-stream";
|
||||
}
|
||||
|
||||
function buildTransportReplayFallback(params: {
|
||||
/**
|
||||
* Provides a narrow replay-policy fallback for providers that do not have an
|
||||
* owning runtime plugin.
|
||||
*
|
||||
* This exists to preserve generic custom-provider behavior. Bundled providers
|
||||
* should express replay ownership through `buildReplayPolicy` instead.
|
||||
*/
|
||||
function buildUnownedProviderTransportReplayFallback(params: {
|
||||
modelApi?: string | null;
|
||||
modelId?: string | null;
|
||||
}): ProviderReplayPolicy | undefined {
|
||||
@@ -162,13 +169,16 @@ export function resolveTranscriptPolicy(params: {
|
||||
model: params.model,
|
||||
};
|
||||
|
||||
const pluginPolicy = runtimePlugin?.buildReplayPolicy?.(context);
|
||||
if (pluginPolicy != null) {
|
||||
return mergeTranscriptPolicy(pluginPolicy);
|
||||
// Once a provider adopts the replay-policy hook, replay policy should come
|
||||
// from the plugin, not from transport-family defaults in core.
|
||||
const buildReplayPolicy = runtimePlugin?.buildReplayPolicy;
|
||||
if (buildReplayPolicy) {
|
||||
const pluginPolicy = buildReplayPolicy(context);
|
||||
return mergeTranscriptPolicy(pluginPolicy ?? undefined);
|
||||
}
|
||||
|
||||
return mergeTranscriptPolicy(
|
||||
buildTransportReplayFallback({
|
||||
buildUnownedProviderTransportReplayFallback({
|
||||
modelApi: params.modelApi,
|
||||
modelId: params.modelId,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user